Stream: ideas

Topic: Optional Record Fields -> Default Valued Record Fields


view this post on Zulip Norbert Hajagos (Feb 24 2024 at 17:30):

Hi all! Quick suggestion: How about we rename "Optional Record Fields" to be "Default Valued Record Fields". My main concern is the tutorial and in general, how we talk about them. Since these record fields can only be used as default values inside record destructuring, I think that would be more appropriate. It would mainly help beginners by communicating that we have no way to define a record with a "missing" field value. There could be a bigger conversation around default / named values based on this comment leading to changes later, but my suggestion could be applied today, making Roc just a bit more clear.

view this post on Zulip Anton (Feb 24 2024 at 18:09):

I agreed with this, until I looked at the type signature we use in the tutorial:

table :
    {
        height : Pixels,
        width : Pixels,
        title ? Str,
        description ? Str,
    }
    -> Table

I can't put my finger on it but in my head it feels weird to try to explain this type signature using the name "Default Valued Fields".

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 18:12):

Probably only cause the default value isn't specified in the type. Instead it is specified separately from the type, but it is required to be specified.

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 18:13):

In the final generated type, those record fields will exist. They are not optional.

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 18:13):

It is just that if they don't exist, they will be created with a default value

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 18:14):

So probably more a gap in syntax then the term "optional record field" being correct.

view this post on Zulip Anton (Feb 24 2024 at 18:16):

Thanks Brendan, that makes sense!

view this post on Zulip Anton (Feb 24 2024 at 18:18):

As you sort of proposed earlier, this also makes me think we should take optional record fields out of the language for now.

view this post on Zulip Anton (Feb 24 2024 at 18:20):

Opinions welcome!

view this post on Zulip Anton (Feb 24 2024 at 18:28):

Optional record fields are not an essential feature and if they don't work well right now it seems sensible to disable them.

view this post on Zulip Brian Teague (Feb 24 2024 at 20:14):

I think @Norbert Hajagos may have found an issue with optional record fields in the other chat. How does Roc handle this scenario, where z has not been defaulted?

Point a : { x: Frac a, y: Frac a, z ? Frac a }

pointToStr : Point -> Frac a
pointToStr = \point ->
    x = point.x
    y = point.y
    z = point.z --Has not been defaulted yet
    Str.concat x (Str.concat y z)

callPointToStr =
    pointToStr { x:1, y:2 } --z is not passed

view this post on Zulip Brian Teague (Feb 24 2024 at 20:16):

@Anton What about when converting JSON object with nested objects, lists, and strings to the equivalent record / list / primitive types that are also nested in Roc?

{
   "fieldA": "value1"
   "fieldB": 2
   "fieldC": {
      "fieldD": [1,2,3]
      "optionalFieldE": "sometimes present"
   }
}

I think optional properties will be needed when deserializing JSON objects

view this post on Zulip Brian Teague (Feb 24 2024 at 20:21):

@Norbert Hajagos @Anton I think the default definition needs to be part of the type definition, not part of the function argument. This would also fix the defaulting issue when renaming variables.

Point a : { x: Frac a, y: Frac a, z: Frac a ? 0.0 }

pointToStr : Point -> Frac a
pointToStr = \point ->
    { x: x1, y: y1, z: z1 } = point --Will be defaulted to 0.0, because of the type definition
    Str.concat x (Str.concat y z)

callPointToStr =
    pointToStr { x:1, y:2 } --z is not passed

view this post on Zulip Norbert Hajagos (Feb 24 2024 at 20:26):

They are not allowed to be used like that. Only in destructuring inside of a function argument. So they are a workaround for default arguments. There is no case when an optional field is not present. Hence the proposal to rename them. They are not optional to be defined. They are optional to be defined for the caller of the function, because they have a default value defined inside the function definition the record is passed to.

view this post on Zulip Brian Teague (Feb 24 2024 at 20:28):

So, point in this example must be destructured as part of the function definition?
pointToStr = \{x, y, z ? 0.0} -> ?

Then what about the example where I have two Point types where I need to rename the variables?
distanceBetweenPoints = \{x: x1, y: y1, z: z1}, {x: x2, y: y2, z: z2} ->

view this post on Zulip Norbert Hajagos (Feb 24 2024 at 20:29):

I have no experience how JSON parsing is done. Someone who has done some Elm should be able to give you an answer. I am pretty sure that it would be either a Tag, (like optionalFieldE: [Some: Str, None]), or it would be a parsing error if that prop should be present all the time

view this post on Zulip Brian Teague (Feb 24 2024 at 20:30):

I've coded several web services before. There are scenarios that happen all the time where fields are optional. And we don't have a dedicated Option Some None type defined in ROC.

view this post on Zulip Brian Teague (Feb 24 2024 at 20:31):

If we make the default value part of the type definition instead of when the instance of a record is created, I think that would pretty much fix all these issues.

view this post on Zulip Norbert Hajagos (Feb 24 2024 at 20:33):

I know Luke Boswell created a JSON parser for Roc, you can find here. I'm sure he would know the answer.

view this post on Zulip Norbert Hajagos (Feb 24 2024 at 20:40):

Brian Teague said:

Then what about the example where I have two Point types where I need to rename the variables?
distanceBetweenPoints = \{x: x1, y: y1, z: z1}, {x: x2, y: y2, z: z2} ->

I don't actually know how you would do that. Haven't tried renaming an optional record field.

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 20:42):

I think it just wouldn't work currently

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 20:42):

I think there is no valid syntax

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 20:48):

As you sort of proposed earlier, this also makes me think we should take optional record fields out of the language for now.

I don't think my opinion is quite that strong. I see the value in them. I think that we should have an alternative before we remove them. I do think default value function arguments are quite useful.

view this post on Zulip Brendan Hansknecht (Feb 24 2024 at 20:56):

As for decoding something like JSON, I think you would need to write a custom Decoding implementation to deal with an optional field. The implementation would be pick between setting a default value or wrapping in some form of optional type. You also could use a generic decoding type that would be something like Dict Str JsonValue to allow for optional fields.

Decoding would not use ?. I don't think it could ever use ?. The issue is that ? is a compile time concept that doesn't exist at runtime. So it wouldn't be able to respond to runtime information of a field in a json existing or not. (technically we could force it to work, but it definitely isn't a planned feature).

we don't have a dedicated Option Some None type defined in ROC.

Quick aside on this. It is trivial to make one with [Some a, None] or by using Result a {}. Roc uses results cause it promotes giving a reason for something. Probably doesn't matter in the case of a JSON field, but you techinically could do Result a [FieldNotFound]. That would be the more what Roc is pushing for.

view this post on Zulip Richard Feldman (Feb 24 2024 at 21:33):

I think it's reasonable to start new threads to discuss alternative designs to optional record fields.

The motivation for the original design was essentially for something like optional keyword arguments, but adding keyword arguments to a language that already had anonymous records seemed redundant. "Just accept a record" is easy, and you don't even need to teach a new concept.

The feature hasn't worked out as well in practice as I had hoped, so I think it's worth discussing alternatives.

view this post on Zulip Richard Feldman (Feb 24 2024 at 21:33):

some notes on alternatives to keep in mind:

  1. I'm open to the idea of having keyword arguments in general, but I'm deeply skeptical that it's right for Roc. We already have a language primitive (anonymous records) that lets you specify labeled arguments, and the performance benefit of maybe putting more things directly in registers sometimes seems very minor compared to doubling the number of ways you can pass arguments to functions in Roc.

view this post on Zulip Richard Feldman (Feb 24 2024 at 21:38):

  1. Something I'm not open to changing about Roc is that you can always define any function without using any type annotations, and also that the compiler can infer that function's type and print it out for you. Separately, it must also be possible to write out the type annotation on its own without any implementation. I think this is a very important part of Roc's design, so (for example) I'm not open to changing function definitions to be Rust-style where the annotation and the arguments are mixed together (like \arg1 : Str, arg2 : Bool -> ...) - it must be possible to implement the function with zero annotations, and it must then be possible for the compiler to infer and print an annotation for that implementation. A good way to check this is to ask: "what if I put a function defined this way into the repl with no types? What would the repl print as the inferred annotation?"

view this post on Zulip Richard Feldman (Feb 24 2024 at 21:45):

  1. I don't think Roc should have optional positional arguments. I looked into this years ago and concluded basically that "if this feature gets used, it leads to worse API design." This is also true of the stronger version of optional arguments, namely multi-arity functions: I don't think Roc should have those either. Relatedly, I also don't think Roc should have an "optional type" (like Str? - some languages call these "nullable types"). These are all plausible alternatives to optional labeled arguments, but I don't think any of them are right for Roc.

view this post on Zulip Richard Feldman (Feb 24 2024 at 23:00):

I think it's also reasonable to discuss the idea of just removing the feature from the language (after all Elm doesn't have it; it's clearly not required!) although that design has downsides too

view this post on Zulip Richard Feldman (Feb 25 2024 at 05:21):

cc @Brian Teague - :point_up: is relevant to other threads where you've commented on related topics!

view this post on Zulip Johannes Maas (Feb 25 2024 at 07:46):

I'd like to offer my perspective as someone who has just stumpled over this.

I think the optional fields are limited in an unintuitive way and they are currently difficult to differentiate from optional fields in other languages. I feel like I fell into the trap of trying to represent an optional value in a data structure with an optional field, because that's how optional fields work in e.g. TypeScript, but that quickly led to problems with constructing the values.

So from this perspective, the optional fields were a stumbling block because they are too easily used where they probably shouldn't be, while having a benefit only in a very specific case.

view this post on Zulip Johannes Maas (Feb 25 2024 at 08:03):

Regarding the downsides, I think there are different approaches to the problems than the optional fields syntax.

What I get from Dillon's post on the downsides are these two issues:

  1. Either people have to look for the the correct default value, import, and use it,
  2. or people have to learn about the weirdly named identity and how to modify the optional values inside a function.

For 1, I think editor tooling could help. When I'm writing out the function and am in the hole for the optional parameters, if the editor would look through the library for a constructor that generates a value of the needed type, it would basically solve 1. If there are multiple constructors, it could list them out. I feel that this is an interesting approach to the problem, since it's quite straightforward to conclude: "I don't want to build all the optional fields, just set one. Ah nice, they provide a set of defaults for it, so I can just modify the ones I need."

For 2 I agree that this is a bit roundabout. Some approaches that come to mind could be to rename the identity function to something like pass as in "pass through" or do something like functionCall requiredArguments Nothing and functionCall requiredArguments (Some (\defaults -> { defaults | field = "override" })).

But the comments mention a third approach that I also find convincing: Builder functions. The necessary arguments are needed to build an instance. Then the optional ones could be functions like buildThing requiredArguments |> withSpecialValue newValue that override the optional values. Since you need to document the required arguments anyway, it should be straightforward to have a list of all the optional functions in the main function's documentation as well, e.g.

Required:
Title: The title displayed at the top.
Description: A description for the item.

Optional:
withSummary: A summary to show instead of the description in previews.
withColor: Override the default color.

So maybe we can explore some of these alternative approaches to the problems with specifying optional arguments. I think the approach where the editor lists the provided constructors seems promising.

view this post on Zulip Richard Feldman (Feb 25 2024 at 12:13):

so today we have:

List.range : {
    start : [At (Num a), After (Num a)],
    end : [At (Num a), Before (Num a), Length (Num a)],
    step ? Num a,
}

I think if we were to remove optional fields, I would just make step mandatory

view this post on Zulip Richard Feldman (Feb 25 2024 at 12:14):

I guess it would be better to look at a current example of a Roc API that uses several optional fields

view this post on Zulip Richard Feldman (Feb 25 2024 at 12:14):

and think about what that would look like in a world where we didn't have optional record fields

view this post on Zulip Richard Feldman (Feb 25 2024 at 12:15):

of note, in Roc { Foo.Bar.baz & field1: blah, field2: etc } does work

view this post on Zulip Richard Feldman (Feb 25 2024 at 12:15):

so that's an option we have that isn't available in Elm

view this post on Zulip Luke Boswell (Feb 25 2024 at 19:10):

I used the optional fields a bit in this repository if you are looking for an example https://github.com/lukewilliamboswell/roc-tinvyvg/blob/5f817b01010e2e67f7fe405e6092cf91a896c672/package/Graphic.roc#L34

view this post on Zulip Eli Dowling (Feb 29 2024 at 06:01):

Quick question. What if we allowed encoding the default into the type itself?
So now you have this:

Http: {method ? GET,code ? 200 ,body: Str}

httpSend:Str->Http->Response
httpSend = \url ,{method, code, body} ->
  # do sending stuff

httpSend "localhost/greet" {body:"hi!"}

That would then be the same as:

httpSend = \url ,{method ? GET, code ? 200, body} ->
  # do sending stuff

httpSend "localhost/greet" {body:"hi!"}

I'm not sure if it's possible, but if it is it would seem to solve this problem.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 06:39):

That was discussed in another thread and has a number of issues: https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Default.20Valued.20Record.20Fields.20in.20Type.20Annotation

view this post on Zulip Eli Dowling (Feb 29 2024 at 08:09):

Ahh, not sure how i messed that one :woman_facepalming:, oops.


Last updated: Jun 16 2026 at 16:19 UTC