Showing posts with label traits. Show all posts
Showing posts with label traits. Show all posts

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.

Monday, June 24, 2013

Introducing: Effes traits

Over the past few points, I've gone over some of my ideas for data types and assumption-types in Effes. The latter were originally intended to be a generalization of the standard concept of statically-checked types, but in my last post I talked about giving up on that idea. In the post before that, I ended things by saying that I wanted to unify some of my types of types, and I also got into some of the function resolution issues that have come up. In this post, I'll try to unify all of those concepts under a new concept that I'm calling a trait.

Effes' traits are different from normal traits in two important ways. Firstly, they're not just add-ons for fundamental types — they are fundamental types. Secondly, whereas traits in statically compiled languages are usually added to types ("Cars are Resellable"), in Effes they can be added to individual objects ("this Car is Resellable"). This is to encourage a dynamic, composition-focused type system.

Here are some examples of traits, using examples I've shown before. In many cases, the examples are unchanged.

-- abstract trait
Sizeable: -- abstract trait
  size : Int

-- another abstract trait
Stack[A]:
  push A : Stack[A]
  pop : Stack[A]
  head : Possible[A]

-- "class-based" implementation of Sizeable
Stack[A] is Sizeable where
  size = case head of
    Nothing : 0
    _ : 1 + pop

-- "object-based" implementation of Sizeable
QuickSize Stack[A]:
  let s : Int = size -- "size" given by "Stack[A] is Sizeable"
  push _ : QuickSize Stack[A] where
    s' = size + 1
  pop: QuickSize Stack[A] where
    s' = size - 1
  size = s

We start off with two abstract traits, which are similar to interfaces in Java. We then add a "class-based" implementation of Sizeable, which says that anything which is Stack[A] is also Sizeable. This is class-based because it applies to all objects whose type is (or includes, in a composite type) Sizeable. Next, we introduce a (conjunctive) composite type QuickSize Stack[A] that provides an O(1) implementation of size

My previous posts had a data type, but I'm now unifying those with traits. This declaration is still legal:

data One[A] = value : A

... but it is now just sugar for a stateful trait:

One[A]:
  let value : A
  value : A = value

Fields in a stateful trait are private to the scope that declared them ("QuickSize Stack[A]:" in the first example, "LinkedNode[A]:" in the second), and within that scope, names resolve to fields before functions (that is, a field always hides a function).

While we're the topic of stateful traits' fields, note that they start with the keyword let and may optionally be assigned a value. If they're assigned a value, this value can't be set except within the scope that defined the field; if they're not, this value must be set when the object is created. This can be done using the syntax TypeName(fieldName = value) or TypeName where fieldName = value. In the first form, multiple fields are delimited by commas, and the field name and = operator are optional if there is exactly one field. In the second form, each assignment must be on a new line, indentented relative to the first type name, but the whole thing can be on one line if there is exactly one field. Some examples:

a = One(value=27)
b = One(27)
c = One where
  value = 27
d = One where value = 27
e = Pair(first=23, second=8)
f = Pair where
  first = 23
  second = 8
-- notice that the two "inlined" form are not
-- available to Pair because it has more than
-- one field

The data syntax is mostly just sugar, but it does allow the creation of stateless, methodless traits like data Nothing.

With this new, unified type system, the function resolution rules I described earlier no longer apply. I think a new system shouldn't be hard, but I'll leave that for the next few posts in the interest of keeping this one from snaking off too far.