code / tomo

Lines41.3K C23.7K Markdown9.7K YAML5.0K Tomo2.3K
7 others 763
Python231 Shell230 make212 INI47 Text21 SVG16 Lua6
(214 lines)

Enums

Tomo supports tagged enumerations, also known as "sum types." Users can define their own using the enum keyword:

enum VariousThings(AnInteger(i:Int), TwoWords(word1, word2:Text), Nothing)

...

a := VariousThings.AnInteger(5)
b := VariousThings.TwoWords("one", "two")
c := VariousThings.Nothing

Pattern Matching

The values inside an enum can be accessed with pattern matching

when x is AnInteger(i)
    say("It was $i")
is TwoWords(x, y)
    say("It was $x and $y")
is Nothing
    say("It was nothing")

Pattern matching blocks are always checked for exhaustiveness, but you can add an else block to handle all unmatched patterns.

Tag Checking

Tags can also be quickly checked using the .TagName field:

assert a.AnInteger != none
assert a.TwoWords == none

Reducing Boilerplate

There are three main areas where we can easily reduce the amount of boilerplate around enums. We don't need to type VariousThings. in front of enum values when we already know what type of enum we're dealing with. This means that we don't need the name of the type for pattern matching (because we can infer the type of the expression being matched). We also don't need the name of the type when calling a function with an enum argument, nor when returning an enum value from a function with an explicit return type:

enum ArgumentType(AnInt(x:Int), SomeText(text:Text))
enum ReturnType(AnInt(x:Int), Nothing)

func increment(arg:ArgumentType -> ReturnType)
    when arg is AnInt(x)
        return AnInt(x + 1)
    is SomeText
        return Nothing

...

assert increment(AnInt(5)) == AnInt(6)
assert increment(SomeText("HI")) == Nothiing

This lets us have overlapping tag names for different types, but smartly infer which enum's value is being created when we know what we're expecting to get. This also works for variable assignment to a variable whose type is already known.

Namespacing

Enums can also define their own methods and variables inside their namespace:

enum VariousThings(AnInteger(i:Int), TwoWords(word1, word2:Text), Nothing)
    meaningful_thing := AnInteger(42)
    func doop(v:VariousThings)
        say("$v")

Functions defined in an enum's namespace can be invoked as methods with : if the first argument is the enum's type or a pointer to one (vt.doop()).

Anonymous Enums

In some cases, you may want to use anonymous inline-defined enums. This lets you define a lightweight type without a name for cases where that's more convenient. For example, a function that has a simple variant for an argument:

func pad_text(text:Text, width:Int, align:enum(Left,Right,Center) = Left -> Text)
    ...
...
padded := pad_text(text, 10, Right)

This could be defined explicitly as enum TextAlignment(Left,Right,Center) with pad_text defining align:TextAlignment, but this adds a new symbol to the top-level scope and forces the user to think about which name is being used. In some applications, that overhead is not necessary or desirable.

Anonymous enums can be used in any place where a type is specified:

  • Declarations: my_variable : enum(A, B, C) = A
  • Function arguments: func foo(arg:enum(A, B, C))
  • Function return values: func foo(x:Int -> enum(Valid(result:Int), Invalid(reason:Text)))
  • Struct members: struct Foo(x:enum(A,B,C)), enum Baz(Thing(type:enum(A,B,C)))

In general, anonymous enums should be used sparingly in cases where there are only a small number of options and the enum code is short. If you expect users to refer to the enum type, it ought to be defined with a proper name. In the pad_text example, the anonymous enum would cause problems if you wanted to make a wrapper around it, because you would not be able to refer to the pad_text align argument's type:

func pad_text_wrapper(text:Text, width:Int, align:???)
    ...pad_text(text, width, align)...

Note: Each enum type is distinct, regardless of whether the enum shares the same values with another enum, so you can't define another enum with the same values and use that in places where a different anonymous enum is expected.

Result Type

One very common pattern for enums is something which can either succeed or fail with an informative message. For example, if you try to delete a file, you will either succeed or fail, and if you fail, you might want to know that it was because the file doesn't exist or if you don't have permission to delete it. For this common pattern, Tomo includes a Result enum type in the standard library:

enum Result(Success, Failure(reason:Text))

You're free to define your own similar enum type or reuse this one as you see fit.

Field Access

In some cases, a full when block is overkill when a value is assumed to have a certain tag. In those cases, you can access the enum's tag value using field access. The resulting value is none if the enum value is not the expected tag, otherwise it will hold the struct contents of the enum value for the given tag.

func maybe_fail(should_fail:Bool -> Result)
    if should_fail
        return Failure("It failed")
    else
        return Success

>> maybe_fail(yes).Failure
# Prints 'Failure("It failed")'
assert maybe_fail(yes).Failure!.text == "It failed"

>> maybe_fail(no).Failure
# Prints 'none'

Enum Assertions

In general, it's best to always handle failure results close to the call site where they occurred. However, sometimes, there's simply nothing you can do beyond reporting the error to the user and closing the program.

result := (/tmp/log.txt).append(msg)
when result is Failure(msg)
    fail(msg)
is Success
    pass

For these cases, you can reduce the amount of code using a couple of simplifications. Firstly, you can access .Success to get the optional empty value of the Result enum (or none if there was a failure) and use ! to assert that the value is non-none.

(/tmp/log.txt).append(msg).Success!

Tomo is smart enough to give you a good error message in this case that will look something like:

This was expected to be Success, but it was:
Failure("Could not write to file: /tmp/log.txt (Permission denied)")

You can further reduce the verbosity of this code by applying the ! directly to the Result enum value:

(/tmp/log.txt).append(msg)!

When the ! operator is applied to an enum value, the effect is the same as applying .Success! or whatever the first tag in the enum definition is.

enum Foo(A(member:Int), B)

f := Foo.A(123)
assert f! == f.A!
assert f!.member == 123
1 # Enums
3 Tomo supports tagged enumerations, also known as "sum types." Users
4 can define their own using the `enum` keyword:
6 ```tomo
7 enum VariousThings(AnInteger(i:Int), TwoWords(word1, word2:Text), Nothing)
9 ...
11 a := VariousThings.AnInteger(5)
12 b := VariousThings.TwoWords("one", "two")
13 c := VariousThings.Nothing
14 ```
16 ## Pattern Matching
18 The values inside an enum can be accessed with pattern matching
20 ```tomo
21 when x is AnInteger(i)
22 say("It was $i")
23 is TwoWords(x, y)
24 say("It was $x and $y")
25 is Nothing
26 say("It was nothing")
27 ```
29 Pattern matching blocks are always checked for exhaustiveness, but you can add
30 an `else` block to handle all unmatched patterns.
32 ## Tag Checking
34 Tags can also be quickly checked using the `.TagName` field:
36 ```tomo
37 assert a.AnInteger != none
38 assert a.TwoWords == none
39 ```
41 ## Reducing Boilerplate
43 There are three main areas where we can easily reduce the amount of boilerplate
44 around enums. We don't need to type `VariousThings.` in front of enum values
45 when we already know what type of enum we're dealing with. This means that we
46 don't need the name of the type for pattern matching (because we can infer the
47 type of the expression being matched). We also don't need the name of the type
48 when calling a function with an enum argument, nor when returning an enum value
49 from a function with an explicit return type:
51 ```tomo
52 enum ArgumentType(AnInt(x:Int), SomeText(text:Text))
53 enum ReturnType(AnInt(x:Int), Nothing)
55 func increment(arg:ArgumentType -> ReturnType)
56 when arg is AnInt(x)
57 return AnInt(x + 1)
58 is SomeText
59 return Nothing
61 ...
63 assert increment(AnInt(5)) == AnInt(6)
64 assert increment(SomeText("HI")) == Nothiing
65 ```
67 This lets us have overlapping tag names for different types, but smartly infer
68 which enum's value is being created when we know what we're expecting to get.
69 This also works for variable assignment to a variable whose type is already
70 known.
72 ## Namespacing
74 Enums can also define their own methods and variables inside their namespace:
76 ```tomo
77 enum VariousThings(AnInteger(i:Int), TwoWords(word1, word2:Text), Nothing)
78 meaningful_thing := AnInteger(42)
79 func doop(v:VariousThings)
80 say("$v")
81 ```
83 Functions defined in an enum's namespace can be invoked as methods with `:` if
84 the first argument is the enum's type or a pointer to one (`vt.doop()`).
86 ## Anonymous Enums
88 In some cases, you may want to use anonymous inline-defined enums. This lets
89 you define a lightweight type without a name for cases where that's more
90 convenient. For example, a function that has a simple variant for an argument:
92 ```tomo
93 func pad_text(text:Text, width:Int, align:enum(Left,Right,Center) = Left -> Text)
94 ...
95 ...
96 padded := pad_text(text, 10, Right)
97 ```
99 This could be defined explicitly as `enum TextAlignment(Left,Right,Center)` with
100 `pad_text` defining `align:TextAlignment`, but this adds a new symbol to the
101 top-level scope and forces the user to think about which name is being used. In
102 some applications, that overhead is not necessary or desirable.
104 Anonymous enums can be used in any place where a type is specified:
106 - Declarations: `my_variable : enum(A, B, C) = A`
107 - Function arguments: `func foo(arg:enum(A, B, C))`
108 - Function return values: `func foo(x:Int -> enum(Valid(result:Int), Invalid(reason:Text)))`
109 - Struct members: `struct Foo(x:enum(A,B,C))`, `enum Baz(Thing(type:enum(A,B,C)))`
111 In general, anonymous enums should be used sparingly in cases where there are
112 only a small number of options and the enum code is short. If you expect users
113 to refer to the enum type, it ought to be defined with a proper name. In the
114 `pad_text` example, the anonymous enum would cause problems if you wanted to
115 make a wrapper around it, because you would not be able to refer to the
116 `pad_text` `align` argument's type:
118 ```tomo
119 func pad_text_wrapper(text:Text, width:Int, align:???)
120 ...pad_text(text, width, align)...
121 ```
123 **Note:** Each enum type is distinct, regardless of whether the enum shares the
124 same values with another enum, so you can't define another enum with the same
125 values and use that in places where a different anonymous enum is expected.
128 ## Result Type
130 One very common pattern for enums is something which can either succeed or fail
131 with an informative message. For example, if you try to delete a file, you will
132 either succeed or fail, and if you fail, you might want to know that it was
133 because the file doesn't exist or if you don't have permission to delete it.
134 For this common pattern, Tomo includes a `Result` enum type in the standard
135 library:
137 ```
138 enum Result(Success, Failure(reason:Text))
139 ```
141 You're free to define your own similar enum type or reuse this one as you see
142 fit.
145 ## Field Access
147 In some cases, a full `when` block is overkill when a value is assumed to have
148 a certain tag. In those cases, you can access the enum's tag value using field
149 access. The resulting value is `none` if the enum value is not the expected tag,
150 otherwise it will hold the struct contents of the enum value for the given tag.
152 ```tomo
153 func maybe_fail(should_fail:Bool -> Result)
154 if should_fail
155 return Failure("It failed")
156 else
157 return Success
159 >> maybe_fail(yes).Failure
160 # Prints 'Failure("It failed")'
161 assert maybe_fail(yes).Failure!.text == "It failed"
163 >> maybe_fail(no).Failure
164 # Prints 'none'
165 ```
167 ## Enum Assertions
169 In general, it's best to always handle failure results close to the call site
170 where they occurred. However, sometimes, there's simply nothing you can do
171 beyond reporting the error to the user and closing the program.
173 ```tomo
174 result := (/tmp/log.txt).append(msg)
175 when result is Failure(msg)
176 fail(msg)
177 is Success
178 pass
179 ```
181 For these cases, you can reduce the amount of code using a couple of
182 simplifications. Firstly, you can access `.Success` to get the optional empty
183 value of the Result enum (or `none` if there was a failure) and use `!` to
184 assert that the value is non-`none`.
186 ```tomo
187 (/tmp/log.txt).append(msg).Success!
188 ```
190 Tomo is smart enough to give you a good error message in this case that will
191 look something like:
193 ```
194 This was expected to be Success, but it was:
195 Failure("Could not write to file: /tmp/log.txt (Permission denied)")
196 ```
198 You can further reduce the verbosity of this code by applying the `!` directly
199 to the Result enum value:
201 ```tomo
202 (/tmp/log.txt).append(msg)!
203 ```
205 When the `!` operator is applied to an enum value, the effect is the same as
206 applying `.Success!` or whatever the first tag in the enum definition is.
208 ```tomo
209 enum Foo(A(member:Int), B)
211 f := Foo.A(123)
212 assert f! == f.A!
213 assert f!.member == 123
214 ```