Thursday, June 27, 2013

Creating composite objects

Up until now, I've talked mostly about types. In this post, I'm going to take a slight detour and talk about objects. This won't be the most jaw-dropping of posts, but it will be helpful for the discussion on subtypes, which is necessary for talking about function resolution.

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, a has an inferred compile-time type of Foo, and the code creates a new Foo object whose foo value is 123. Yawn. Then, b has an explicit compile-time type of Foo | Bar, and the code again creates a Foo object. 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 Car is 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 c assignment 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 Bar type 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 where syntax, 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 Bar as 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.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.