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.
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:
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.
The type for instantiation can also be specified with an interpolation:
The interpolation can contain any expression that resolves to a Type.
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.
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
.
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:
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.
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.
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:
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
:
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:
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:
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.
Just like Modules, Types can be re-opened to append to their definition. For example, starting with a simple definition of a Type:
Later on, potentially even in another file, this type could be re-opened to add new methods to it:
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:
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.
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.
extend
can be useful for quickly implementing class-level DSLs such as schema definitions or or common patterns like the Factory pattern.