Chapter 7: Types and Self

Beyond Modules for grouping values together into a namespace, Myst also provides a mechanism for defining Types and Instances.

Types are useful for defining complex data structures and ways to interact with that data more cleanly than could be done with Maps or other values.

Every value in Myst is an instance of a type. For example, the number 1 is an instance of the type Integer, and "hello, world" is an instance of String.

The majority of popular object-oriented languages refer to types as "classes". In practice there is no distinction between the two - the name "type" is simply a preference of the language.

Definition and Instantiation

To define a Type, Myst uses the deftype keyword. The structure is similar to Modules, where the name must be a Constant, and the body is any series of expressions. A simple example of a type might look like this:

1deftype Car
2end

To create an instance of this Type, Myst uses a special syntax that resembles Struct instantiation from Elixir. For example, creating an instance of the Car class would look like this:

1%Car{}

The result of instantiation is a new Instance value of the Type specified by the instantiation. These values are just like any other value, and can be used as such.

1my_car = %Car{}
2buy_for(%Car{}, 10_000.00)

The type for instantiation can also be specified with an interpolation:

 1deftype Foo
 2  def call; :foo; end
 3end
 4
 5deftype Bar
 6  def call; :bar; end
 7end
 8
 9def make_instance(type)
10  %<type>{}
11end
12
13f = make_instance(Foo)
14b = make_instance(Bar)
15f.call #=> :foo
16b.call #=> :bar

The interpolation can contain any expression that resolves to a Type.

Instance Variables and Initialization

When a new Instance of a Type is created, it goes through initialization, where the properties of the instance can be configured. This can be controlled by defining an initialize instance method on the Type.

1deftype Car
2  def initialize
3    # configure the new instance here.
4  end
5end

If no initialize method is given on a Type, instances of that Type will be created with no default configuration.

Instance variables are the properties of a Type. In our Car example, we could add instance variables for the color, the number of doors, the body style, etc. Every Instance will have its own set of instance variables.

Instance variables in Myst are created and referenced with identifiers that are prefixed with an @. By default, the instance variables of a Type are private, and must be exposed by methods on the Type to be made public. Attempting to access an instance variable before it has been set will create the variable with an initial value of nil.

 1deftype Car
 2  def initialize(color, door_count)
 3    @color      = color
 4    @door_count = door_count
 5  end
 6
 7  def color; @color; end
 8  def color=(c); @color = c; end
 9
10  def door_count; @door_count; end
11  def door_count=(dc); @door_count = dc; end
12end

Here, Car has two instance variables, @color and @door_count. Each instance will have it's own copies of these instance variables. The instance variables are created as soon as they are assigned.

The initialize method above accepts two parameters. Arguments are passed to initialize by listing them between the curly braces used in instantiation:

1  sedan = %Car{"gray", 4}
2  coupe = %Car{"red",  2}

The groups of methods at the bottom of this definition of Car are generally referred to as getters and setters for the instance variables. These expose the private instance variables as public properties that can be interacted with.

With the way Myst handles Calls and assignments, these methods essentially allow us to treat the properties of the Type as normal variables. For example, updating the color of the sedan instance from above can be done just like a normal assignment:

1sedan.color = "blue"

These Calls also work with operational assignments, and any other places where variables are used:

1sedan.door_count += 2 #=> 6

An important note is that instance variables are not valid outside of instance methods. Using an instance variable in the type definition body or anywhere else will likely cause unexpected behavior.

Self

When dealing with Instances, there are often cases where methods would like to reference the object that they belong to. To accomodate this, Myst provides a special self variable, which refers to that object.

1deftype Foo
2  def reflect
3    self
4  end
5end
6
7f = %Foo{}
8f.reflect == f #=> true

self can also be used to disambiguate method calls from local variables. In particular, this is useful for calling getters and setters from initializers. Consider this code:

 1deftype Person
 2  def initialize(given_name)
 3    full_name = given_name
 4  end
 5
 6  def full_name; @full_name; end
 7  def full_name=(full_name)
 8    @full_name = full_name
 9    [@first_name, @last_name] =: full_name.split
10  end
11
12  def first_name; @first_name; end
13  def last_name;  @last_name; end
14end
15
16p = %Person{"Freddy Mercury"}
17p.full_name   #=> nil
18p.first_name  #=> nil
19p.last_name   #=> nil

Something that may or may not be obvious here is that the full_name setter (full_name=) will not be called, and the @full_name instance variable will not be assigned. Instead, the assignment in the initializer is interpreted as a new local variable full_name, rather than a call to the full_name= method.

One solution to this problem would be to assign the @full_name instance variable directly in the initializer:

 1def initialize(given_name)
 2  @full_name = given_name
 3end
 4
 5# ...
 6p = %Person{"Freddy Mercury"}
 7p.full_name   #=> "Freddy Mercury"
 8p.first_name  #=> nil
 9p.last_name   #=> nil

However, there could be some cases where there is added functionality for setting a variable, and calling the setter method is more desirable. For example, our full_name= setter for the Person type above also assigns the @first_name and @last_name instance variables. Without calling the setter, these variables are not assigned.

The other solution is to use self to ensure that the assignment is interpreted as a call to full_name:

 1def initialize(given_name)
 2  self.full_name = given_name
 3end
 4
 5# ...
 6p = %Person{"Freddy Mercury"}
 7p.full_name   #=> "Freddy Mercury"
 8p.first_name  #=> "Freddy"
 9p.last_name   #=> "Mercury"

Static Methods

In addition to instance methods, Types can also define static methods. Static methods are methods that can be called from the Type itself, rather than instances of the type. In this sense, static methods essentially act like methods on a Module.

To define static methods, Myst uses the defstatic keyword:

1deftype Foo
2  defstatic foo
3    :static_foo
4  end
5end
6
7Foo.foo #=> :static_foo

One common use case for static methods is defining new ways of constructing instances. For example, we could define various methods for instantiating different types of Cars:

 1deftype Car
 2  defstatic coupe(color)
 3    %Car{color, 2}
 4  end
 5
 6  defstatic sedan(color)
 7    %Car{color, 4}
 8  end
 9
10  def initialize(color, door_count)
11    @color = color
12    @door_count = door_count
13  end
14
15  def color; @color; end
16  def door_count; @door_count; end
17end
18
19c = Car.coupe("blue")
20c.door_count = 2
21s = Car.sedan("gray")
22s.door_count = 4

Inside of static methods, self refers to the Type being modified. This can be useful when creating instances of the type, helping to avoid potential refactoring issues:

 1deftype Foo
 2  defstatic create
 3    %<self>{}
 4  end
 5
 6  def to_s
 7    "foo"
 8  end
 9end
10
11IO.puts(Foo.create) #=> "foo"

This example uses the interpolated instantiation syntax (%<self>{}) to dynamically specify the type for instantiation. This avoids explicitly repeating the name Foo, which could help avoid errors when refactoring the name of the class.

Re-opening Types

Just like Modules, Types can be re-opened to append to their definition. For example, starting with a simple definition of a Type:

 1deftype Foo
 2  def initialize(value : Integer)
 3    @value = value
 4  end
 5
 6  def add(other : Integer)
 7    @value += other
 8  end
 9end
10
11foo1 = %Foo{10}
12foo1.add(2)

Later on, potentially even in another file, this type could be re-opened to add new methods to it:

1deftype Foo
2  def sub(other : Integer)
3    @value -= other
4  end
5end
6
7foo2 = %Foo{10}

Now, the type Foo has both an add method and a sub method. All instances of Foo (even ones that were created before the Type was re-opened) can use both of these methods freely:

1foo2.add(3)
2foo1.sub(4)

Include and Extend

Just like Modules, Types can include other Modules to bring in new functionality. Using include in a Type will add the Module's methods as instance methods on the Type.

 1defmodule Foo
 2  def foo
 3    :module_foo
 4  end
 5end
 6
 7deftype Bar
 8  include Foo
 9end
10
11%Bar{}.foo #=> :module_foo

In addition, Myst provides a mechanism for adding Modules as static methods using the extend keyword. The result of extend is essentially the same as using include from within another Module.

 1defmodule Foo
 2  def foo
 3    :module_foo
 4  end
 5end
 6
 7deftype Bar
 8  extend Foo
 9end
10
11Bar.foo #=> :module_foo

extend can be useful for quickly implementing class-level DSLs such as schema definitions or or common patterns like the Factory pattern.

Get Started

Introduction Values and Variables Basic Operations Flow Control Pattern Matching Functions and Clauses Modules Types and Self Blocks and Anonymous Functions Exception Handling Loading Code