Optics are great for updates.
After the episode I gave records some more thought:
newrec Foo a =
{ name :: String
, value :: a
, bar :: Bar
}
-- new keyword, newrec
-- generates type Foo a, data constructor Foo, and says Foo is a record
-- fields are name, value and bar, there is no function pollution
-- only one data constructor, one type, no need for partial functions
Foo {name = "foo", value = 1, bar = newBar} -- creation
-- getting a value
r {Foo | name}
-- getting a nested value
r {Foo | bar.baz.name}
-- if bar is not a newrec, fail
-- if bar is a newrec, follow the fields
-- no need to disambiguate further, you can follow the record definitions
-- this fails because name is String, and String is not a record
r {Foo | name.bar}
-- this fails because xyz is not a field
r {Foo | xyz.bar}
-- we lost OverloadedRecordDot and HasField in order to disambiguate, maybe we can compensate?
-- returning a tuple with multiple values, can be nested
r {Foo | name, bar, bar.baz.name}
-- returns (name, bar, bar.baz.name)
-- maybe lists or other records/types could be returned
-- other Foo constructor clashes with it? qualify the module
r {M2.Foo | name}
-- multiple updates with nested update
r {Foo | name = "hello", value = 1, bar = newBar, baz.quux.etc = 1}
-- this is illegal, assignment and update? or maybe assign and then update, might confuse user
r {Foo | name = "hello", value = 1, bar = newBar, bar.name = "b"}
-- tired of repeating nesting indexes?
r { Foo
| name = "hello"
, baz.a1.b1 = 1
, baz.a1.b2 = 2
, baz.a1.b3 = 3
}
-- same thing
r {Foo | name = "hello", baz.a1 := {b1 = 1, b2 = 2, b3 = 3}}
-- thanks to Foo disambiguating we can find the other data constructors by following the newrecs
-- we search for a Foo in visible newrecs
-- for each field, we got its type, no type checking or inference needed
-- if we need to go deeper, the type of the field must be a visible newrec
-- if it is not, we fail because it is not a record
-- if it is, we got the next data constructor and its fields
-- therefore, ALL the expressions can be written by GHC as nested cases
r {Foo | name = "hello", value = 1, bar = newBar, baz.quux.etc = 1}
=> case r of Foo a1 a2 a3 a4 -> Foo "hello" 1 newBar (a4 {Baz | quux.etc = 1})
=> Foo "hello" 1 bar (case a4 of Baz b1 b2 -> Baz (b1 {Quux | etc = 1}) b2)
=> Foo "hello" 1 bar (case a4 of Baz b1 b2 -> Baz (case b1 of Quux c1 -> Quux 1) b2)
r {Foo | name, bar, bar.baz.name}
=> case r of Foo a1 a2 a3 -> case a3 of Bar b1 b2 -> case b2 of Baz c1 -> (a1, a2, c1)
r {Foo | name, bar.baz, quux.awk}
=> case r of Foo a1 a2 a3 -> case a2 of Bar b1 b2 -> case a3 of Quux c1 -> (a1, b2, c1)
What if they worked like this? This could be an extension.