I'll start by knocking out two really easy cases: uncomposed types and union types.
data Foo = Foo(foo : Int) data Bar a = Foo(123) -- uncomposed type b : Foo | Bar = Foo(456)
In this example,
ahas an inferred compile-time type of
Foo, and the code creates a new
123. Yawn. Then,
bhas an explicit compile-time type of
Foo | Bar, and the code again creates a
Fooobject. Yawn again. But all of a sudden...
c : Foo Bar = Foo(789)
Ah, this is interesting. The object being created is a
Foo, and yet it's being assigned to a
Foo Bar, which is essentially a subtype of
Foo! This would be like a snippet of Java code reading
Car c = new Vehicle()(if
Caris a subclass of
Vehicle). That's not allowed in Java, so why would it be in Effes?
What's really going on in that example is this:
fTmp : Foo = Foo(789) bTmp : Bar = Bar() c : Foo Bar = fTmp bTmp
Just as a conjunctive composed type is created by just writing its two component types side-by-side, a composed object is created by writing its two component objects side-by-side. Simple as that!
The original syntax of the
cassignment was actually sugar. If the right-hand side of an assignment is of type A, and the left-hand side is of type A B, and B is a type which doesn't require any explicitly-provided state, then an object of type B is assumed. That is, the original
c : Foo Bar = Foo(789)was automatically expanded to
c : Foo Bar = Foo(789) Bar()because the
Bartype doesn't require any explicitly-provided state.
What happens if you do need to add state? You have two options, both analogous to the syntax for constructing uncomposed objects. You can use the parenthetical constructor syntax for each uncomposed object, and just list the objects side-by-side (this is the syntax I've been using in this post so far). You can also use
wheresyntax, listing all of the fields you want to set in the indented field-assignment block. You can prefix any field name with its type to qualify it; this is optional in most cases, but required when field names collide.
data Foo = Foo(foo : Int, fizz : String) data Bar(bar : Int, fizz : Int) fooBar1 = Foo(123, "Hello") Bar(456, 789) fooBar2 = Foo Bar where foo = 123 -- could have been 'Foo foo' Foo fizz = "Hello" bar = 456 -- could have been 'Bar bar' Bar fizz = 789
Note that the order of these fields doesn't matter — you can interleave them, whatever. Each field's name unambiguously identifies it, and they all belong to the single type
Foo Bar, so there's nothing special about any particular order. That said, it's probably good form to group fields by component type.
I'm considering adding an additional syntax, which really treats
Foo Baras the new, separate type it is:
fooBar3 = (Foo Bar)(foo=123, Foo fizz="Hello", bar=456, Bar fizz=789)
This has a certain symmetric elegance to it, but it's kinda ugly and potentially confusing. I was going to disallow it, but I think I have to let it in because of nicknames. While the example above is ugly, this starts to make sense:
nickname FooBar = Foo Bar fooBar4 = FooBar(foo=123, Foo fizz="Hello", bar=456, Bar fizz=789)
I suppose I could allow that syntax only if the composite type has been nicknamed (and is being constructed as such), but this feels like it would complicate the language for not much gain. Better to say that the
(Foo Bar)(...)syntax is allowed but discouraged. At least, I hope that's better.