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.
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".
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.
In the final generated type, those record fields will exist. They are not optional.
It is just that if they don't exist, they will be created with a default value
So probably more a gap in syntax then the term "optional record field" being correct.
Thanks Brendan, that makes sense!
As you sort of proposed earlier, this also makes me think we should take optional record fields out of the language for now.
Opinions welcome!
Optional record fields are not an essential feature and if they don't work well right now it seems sensible to disable them.
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
@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
@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
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.
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} ->
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
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.
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.
I know Luke Boswell created a JSON parser for Roc, you can find here. I'm sure he would know the answer.
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.
I think it just wouldn't work currently
I think there is no valid syntax
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.
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.
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.
some notes on alternatives to keep in mind:
\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?"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.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
cc @Brian Teague - :point_up: is relevant to other threads where you've commented on related topics!
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.
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:
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.
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
I guess it would be better to look at a current example of a Roc API that uses several optional fields
and think about what that would look like in a world where we didn't have optional record fields
of note, in Roc { Foo.Bar.baz & field1: blah, field2: etc } does work
so that's an option we have that isn't available in Elm
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
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.
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
Ahh, not sure how i messed that one :woman_facepalming:, oops.
Last updated: Jun 16 2026 at 16:19 UTC