#@field in typescript SDK
1 messages ยท Page 1 of 1 (latest)
Thanks, reading now
is that explanation also available in the "option B"?
What is a field? What's the difference between a field and function?
Is "field" a typescript concept?
Why do I need decorators at all? What happens if I just add a method to my object? Why isn't that enough to make it a function?
A field is a variable that is part of the Dagger state, a function is a callable field that can execute logic
It's the same in Python, and in Go with public variable
Because exposing something to the dagger api is explicit in typescript and python
OK, they all seem redundant to me, but maybe it's because I'm not used to typescript
I look at a function, and there's a decorator saying "this is a function"
Dagger will only expose the fields of a modude that are explicitely exposed using the @field() decorators, the rest will stay private in the context of your class.
I look at a class and there's a decorator saying "this is an object"
Saying this is a func to expose to the Dagger API
This is an object to expose to the dagger api
Yeah I understand.
everything is explicit basically
Same
There's a concept of state
fields are the state
Public variable in Go are part of the state, in ts and python it's identified with the @field decorator
OK. Where's the documentation explaining this?
so you can call the field using dagger call
This should be part of a global documentation
/cc @ashen sage
I only explain the convention for Typescript, because I assume we'll have a global doc explaining that
I think we have a problem of passing on major DX problems onto documentation
Yep we mainly focused on feature and implementation, the doc isn't ready yet
We're still looking for the right structure
@grand niche You might be interested in this thread too
Go does not have the word "field" used in a Dagger-specific way
The end user
Wrong
I want the SDK to serialize my Go struct fields, but I don't necessarily want those fields to be visible to the end user as functions
If I have a public variable in a Go struct, I can access it from the dagger API using dagger call no?
(we don't expose a concept of fields to the end user)
So why the end user can access them with dagger call?
by default yes but I can hide it with // +private. In any case if it is exposed, it is as a function. There is no end-user-facing concept of field
In any case, you answered my original question, so thanks
In Typescript and Python, it's the reverse, everything is // +private except if you tell explicitely dagger to expose it
Field is just the way to declare it, we could have name it @property or @state or anything
Yes but by saying @field as something different than @function, you're implying that there are two different things you can expose to the end user: a field, or a function. That is incorrect.
If it's exposed to the end user, it's a function
That would have been strange to decorat a variable @func
The other way around is to remove these decorators and not expose things that has the keyword private, which exist in both language
And about the class, theses that have the keyword export
But then, we cannot support alias or we would need a decorator @alias
However, I think being explicit with @field and @func is a pretty nice way to understand dagger: a module has a state composed of fields and a set of functions that are exposed to the Dagger API and callable by the end user
That's a wrong understanding of dagger though
What's the right one?
What I explained before
But it's not actually true, Dagger use GraphQL to expends schema and a GraphQL has a concept of fields and functions
Your property can be persisted, or it can be exposed as a function, or both
wrong
{
myFunc(someArg: "aaa") {
field
}
}
In a GQL query, the field isn't a function, it's a field
GraphQL has a concept of field. It has no concept of functions.
What GraphQL calls fields, we call functions.
Confusing I know ๐
But, you can call function in GQL? I don't understand
You can replace field with field(), they are equivalent in GQL
every graphql field is like a function. As a convenience, if you pass no arguments, you can skip the ()
Ah ok I didn't know that
It's definitely confusing because of the overlapping use of the same words in different parts of the stack
Well then this ^^ is a way to be in phase with what you said
So it's the same as in Go, everything is a field (so a function) except if you don't want to by adding a keyword
That would require a LOT of refactor tho...
We definitely don't have time for a refactor before the release...
All I can do is clarify, and complain ๐
There's another level of confusion here because solomon isn't aware there's actually the concept of field in our API.
So we make a difference between fields and functions
I am aware, and disapprove ๐ as discussed elsewhere
fields should be purely a concern between SDK and engine
And in practice, today it is
But I worry that will not remain the case, because of the confusion
Yeah the description fo FieldTypeDef in that API doc is definitely wrong. It talks about static vs. dynamic, which is not the point of having fields. The only point is state serialization
Yeah, I hear you. Wasn't as clear to me until recently. But makes it weird when mapping to a language. That internal concept of field was because we wanted to turn struct fields into functions without having to create getters. We can say that a class's properties are conveniently turned into functions. With that argument maybe it's not too weird to use a @func decorator in a class property.
Right, saying things like "conveniently exposed as functions" would definitely help
I find it pretty weird since our internal API has a concept of field
Even without changing the current DX, if we said @field: expose this property as a persistent field of the Dagger object, to be persisted by the engine in between calls. By default, the property will also be exposed as a function, This can be disabled with ???
In Python it's a bit different. The @object_type decorator is a wrapper on a @dataclass. Similarly, the "field" in name: str = field() is also a wrapper on top of dataclass.field. So there's a language specific explanation on why it's called "field", without having to say it's a Dagger field.
only for the purpose of SDK-to-engine communication (for persisting object state)
In our case, it's disabled if you don't put @field notation
You can say that if you don't put @field it just doesn't create a getter function in the API for you, but if there's concern about exposing the internal concept of state, we can just not talk about it.
It's fine to talk about it, since it does affect how your module works. It would be even better if we find a way to clarify the distinction between 1) functions that the client can call, and 2) state that the engine persists for you
But 2) is basically what module fields are. So I'm at a loss on how to reconcile here.
Fields do two things: 1) persist state between SDK-to-API calls; 2) create a corresponding getter function to access that state.
We could also do something release like it is right now and add a big refactor for v0.11 where you don't need decorators anymore -> fields and functions become implicit like in Go?
Maybe just saying that is enough, then
Simply saying that a field will get (by default) a corresponding getter function, helps understand that field and function is not the same. It opens the possibility that there could be a field without a getter function, and then the user couldn't access it
I don't think that part was clear to @grim dirge before this conversation
No, all fields create a corresponding getter function. If not, it's not a dagger field.
Then what does // +private do in the Go SDK?
+private simply doesn't expose the struct field as an "API field".
lol
so there's "state fields" and "API fields" now?
There's a language's object state, in memory, and there's API state in the server.
No, // +private just doesn't expose the getter function, that's it. It's still just state that gets passed around
And then there's what I can see with dagger functions
OK I'm not crazy then! I was starting to wonder ๐
I just used // +private today, and it did seem like it was doing what I expected, which is 1) continue to persist the field's state. while 2) removing the convenience getter function for it
I think the confusion is that today it's implement by just never telling the engine about //+private fields, as opposed to telling the engine about and saying it's a private field. But it creates the same end effect, total implementation detail (and we may need to modify this in the future, so the engine knows about it but just doesn't create a getter)
How does that work with typedef system? Do you still call withField on that?
But if the engine doesn't know about it, how does it get persisted?
It's because the results of graphql functions resolvers are just generic "result maps" (typically serialized as json objects). If you put some object keys in that json that aren't officially in the schema, they still get passed around transparently. This is used all the time in the standard js gql server, it works in the go one we used to use, dagql, etc.
Oh this wasn't fresh on my mind at all ๐
But Python supports the same.
I don't think the gql spec explicitly says "thou shalt support this" but its in practice a convention that seems to be universal
Same for Typescript
We even save in the state variables that have the keyword private
So @field == objectTypeDef.withField --> creates a getter function
Still have serialization without it.
maybe if we start talking about "object state" instead of "fields" it would help clarify the terminology over time?
If we had time to refactor before release (which we don't) I would probably use @state instead of @field; and by default not expose a getter function.
(or maybe a bad idea since we have '"field" firmly embedded in the API already)
As long as we agree to talk about "functions" as the only way for a client to query a module, I'm happy
To not expose a getter function in Python or Typescript, just don't use the decorator. Meaning it's a docs change. Only Go is defaulting to it.
But what if I want to persist that property in my Dagger state, but not expose the getter function? How do I do that?
In Python and Typescript? Still works. The decorator is just to create the function.
Yep, everything is saved in the state
For example, in Python:
@object_type
class HelloWorld:
msg: str = "hi"
@function
def say(self) -> str:
return self.msg
Here, msg is still persisted as state because Python will include the "msg" property when it serializes the object.
Just to make sure I understand:
In the Typescript SDK, @field has no effect on whether a property is persisted in the state or not? All properties of an object are always persisted?
So @field only exposes that property as a getter function.
Correct?
Yes
You need to additonally do this to create the getter function:
- msg: str = "hi"
+ msg: str = field(default="hi")
OK I understand
As I said ^^ @field is just a matter of exposing it to the end user or not
Then I propose that @field be deprecated by @function. Not a release blocker of course.
This is why I talk about it in field exposition section
Same for Python?
I don't know enough about the Python SDK to have an opinion. But you can probably determine what I would answer if I did.
I can't use the same @function decorator for what field does. Need a different name. As I said before, it wraps Python's dataclass.field so to access necessary features from there, users will need to use dataclass.field rather than dagger.field in many cases. Python users know what a dataclass field is and how to work with it.
But I'm ok changing the name, just can't use the same function name.
Can't have a func and a function. Can't change both to the same otherwise we're on square one.
It needs to represent what it actually does in comparison to when you don't use it. Basically creates the getter function (i.e., calls obj.withField).
@expose?
How about this? With the Strawberry library, you can define GraphQL resolvers either from a property:
@strawberry.type
class Query:
last_user: User = strawberry.field(resolver=get_last_user)
Or a method:
@strawberry.type
class Query:
@strawberry.field
def last_user(self) -> User:
return User(name="Marco")
I can use the same approach. Needs quite a bit of refactoring but I can work with that.
It would allow keeping the same name:
@dagger.object_type
class Hello:
msg: str = dagger.function(default="hi")
@dagger.function
def greet(self) -> str:
return self.msg
But if you don't want to create the getter function, you may need to use "field" anyway:
class Hello:
- msg: str = dagger.function(default="hi")
+ msg: str = dataclass.field(default="hi")
dataclass.field would be needed to serialize the state I guess? So, different from typescript in that way.
Yes. In the example above it's not necessary, but if your default value is mutable (like []) then you need a factory/callback, in which case you need dataclass.field. Just an example as there's other settings for it. More realistic:
class Hello:
- msgs: list[str] = dagger.function(default=list)
+ msgs: list[str] = dataclass.field(default_factory=list)
Is it clear to say that dagger.field is like a dataclass.field but which also creates a getter function for it in the API?
I would say it depends how familiar and standard dataclass.field is to python devs. If it's as familiar as go's convention for json marshalling of structs, then perhaps dagger.field is not needed
If dagger.function could be made to work with both fields (to add getter function) and methods, then that serms to be the best option, best of both worlds
I found this thread very helpful and instructive, thank you all for your patience ๐
I would say it depends how familiar and standard
dataclass.fieldis to python devs.
It's quite familiar, yes ๐ It's from the standard library.
If
dagger.functioncould be made to work with both fields (to add getter function) and methods, then that serms to be the best option, best of both worlds
The only problem with that is I don't have time to make that big a change for v0.10.0.
oh yeah, to be clear I don't think we should make any major change to the SDKs for the release
But we can do it later and deprecate dagger.field.
It does help to discuss future design changes now, because it can inform what we say in the docs now. We can put the right "lens" on the current technical reality.