Best demonstrated with an example
let response = Response(status: 200, headers: [tuple("content-type", "text/plain")], body: "Hello")
let response = Response(headers: [tuple("foo", "bar"), ..response.headers], ..response)
It's really helpful you can destructure part of a record. it would be useful to be able to update individual fields.
Using the .. operator in the way above when constructing records looks similar to me to how it works when using the .. to construct lists
Makes sense! I have been thinking the same.
Having the spread at the end of the record feels a little bit weird to me for two reasons:
1) I need to read the entire line to realize that the record is being updated, rather than created
2) My intuition is that the keys are set on the record in the order that they're written. Having the spread at the end, then, would overwrite all of the updated keys with the original ones. This is the behaviour in Javascript, for example.
Off the top of my head, the languages that put the original record at the beginning include Javascript, Elixir, Haskell, and Elm. Are there any that have the spread at the end?
I do think that using .. is a natural choice, because of the symmetry with list construction, but I think that the following might be a more familiar syntax:
let response = Response(status: 200, headers: [tuple("content-type", "text/plain")], body: "Hello")
let response = Response(..response, headers: [tuple("foo", "bar"), ..response.headers])
I don't have strong opinions on syntax at the moment :)
Elm has it at the start: { cat | name = "Nubi" }
OCaml has it at the start: { cat with name = "Nubi" }
Rust has it at the end: Cat { name: "Nubi", ..cat }
Erlang has it at the start Cat#cat{name = <<"Nubi">>}
Reason has it at the start { ...cat, name: "Nubi" }
Scala Shapeless has it at the start, though the syntax is very different: cat + (name -> ("Nubi"))
One thing to note is that we use spread at the end of a record when destructuring.
For what it's worth, Haskell has three separate ways of doing adjacent things:
say we have
data MyRecord = MyConstructor { x :: Int
, y :: Text
}
RecordWildCards enables us to do myFun MyConstructor{..} = …
to export all the fields of the record in the function body, and
myFun = MyConstructor{..}
where
x = …
y = …
to "fill" the record with variables that are declared in the scope.
NamedFieldPuns allows to bind record fields to scoped variables of the same namemyFun MyConstructor{x, y} = …
and finally the pretty banal way to update records:
let record = MyConstructor{x = 3, y = "Hello"}
in record{y = "World"}
I am personally very partial towards a solution like the haskell record update syntax:
let response = Response(status: 200, headers: [tuple("content-type", "text/plain")], body: "Hello")
response{headers: tuple("foo", "bar")}
That being said, if we are entering the (rich, complex and sometimes dangerous) field of deeply nested and complex record updates, then an optics library would be warranted.
Thanks for the writeup! I wasn't aware of all of those syntaxes. I just chatted with @lpil briefly on IRC, and I think I'm going to try implementing the following syntax:
let response = Response(status: 200, headers: [tuple("content-type", "text/plain")], body: "Hello")
let response = Response(..response, headers: [tuple("foo", "bar"), ..response.headers])
Thanks @QuinnWilton, @Kleidukos !
is this a 0.10.0 milestone?
Not specifically, but if it's ready by then it'll be released. I'm not sure what the scope for the next release will be yet.
{ cat with name = "Nubi" }
OCaml has it at the start, the with is like the | in Elm. :-)
Does it really make sense for user have to write Response() again?
C# 9.0 is going to have records and currently syntax looks like this var otherPerson = person with { LastName = "Hanselman" };
This looks cleaner let response = response with (headers: [tuple("foo", "bar"), ..response.headers]) to what is currently proposed.
The update syntax has been picked to match the existing list prepend, record construction, and record destructuring spread syntaxes.
Currently we don't have anything similar to the C# syntax there so I feel the existing syntax is more consistent.
Ok but it's just feels odd to me you have to use Response() again to just update record.
It's fairly common to re-type the name, Rust, Elixir, Erlang, and Haskell all have syntaxs like this.
Elixir? I've only used it in hobby projects but Its struct update syntax is %{my_struct | my_field: :foo} so you don't have to write struct type again.
That's the general map update syntax, if you want to update specifically that struct the %MyStruct{my_struct | my_field: :foo} syntax is used. Using the bare map update syntax means you could add a field that does not exist in that struct, resulting in it no longer being valid.
Elixir? I've only used it in hobby projects but It's struct update syntax is %{my_struct | my_field: :foo} so you don't have to write struct type again.
Elixir has both syntaxes, with slightly different semantics. Your example updates a map (but because structs are maps under the hood, it still works for structs). You could also write %Response{response | code: 200}, and it will assert that you're actually returning the correct struct.
Edit: Whoops @lpil beat me to it :)
Oh :) Didn't know that, thanks for the info.
Most helpful comment
Elixir has both syntaxes, with slightly different semantics. Your example updates a map (but because structs are maps under the hood, it still works for structs). You could also write
%Response{response | code: 200}, and it will assert that you're actually returning the correct struct.Edit: Whoops @lpil beat me to it :)