aboutsummaryrefslogtreecommitdiff
path: root/learnxiny.tm
blob: fafccbffe2b9db68c19756d596bcd69775d108ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# Tomo is a statically typed, garbage collected imperative language with
# emphasis on simplicity, safety, and speed. Tomo code cross compiles to C,
# which is compiled to a binary using your C compiler of choice.

# To begin with, let's define a main function:
func main():
    # This function's code will run if you run this file.

    # Print to the console
    say("Hello world!")

    # You can also use !! as a shorthand:
    !! This is the same as using say(), but a bit easier to type

    # Declare a variable with ':=' (the type is inferred to be integer)
    my_variable := 123

    # Assign a new value
    my_variable = 99

    # Floating point numbers are similar, but require a decimal point:
    my_num := 2.0

    # Strings can use interpolation with the dollar sign $:
    say("My variable is $my_variable and this is a sum: $(1 + 2)")

    say("
        Multiline strings begin with a " at the end of a line and continue in
        an indented region below.
          You can have leading spaces after the first line
          and they'll be preserved.

        The multiline string won't include a leading or trailing newline.
    ")

    # Docstring tests use ">>" and when the program runs, they will print
    # their source code to the console on stderr.
    >> 1 + 2

    # If there is a following line with "=", you can perform a check that
    # the output matches what was expected.
    >> 2 + 3
    = 5
    # If there is a mismatch, the program will halt and print a useful
    # error message.

    # Booleans use "yes" and "no" instead of "true" and "false"
    my_bool := yes

    # Conditionals:
    if my_bool:
        say("It worked!")
    else if my_variable == 99:
        say("else if")
    else:
        say("else")

    # Arrays:
    my_numbers := [10, 20, 30]

    # Empty arrays require specifying the type:
    empty_array := [:Int]
    >> empty_array.length
    = 0

    # Arrays are 1-indexed, so the first element is at index 1:
    >> my_numbers[1]
    = 10

    # Negative indices can be used to get items from the back of the array:
    >> my_numbers[-1]
    = 30

    # If an invalid index outside the array's bounds is used (e.g.
    # my_numbers[999]), an error message will be printed and the program will
    # exit.

    # Iteration:
    for num in my_numbers:
        >> num

    # Optionally, you can use an iteration index as well:
    for index, num in my_numbers:
        pass # Pass means "do nothing"

    # Arrays can be created with array comprehensions, which are loops:
    >> [x*10 for x in my_numbers]
    = [100, 200, 300]
    >> [x*10 for x in my_numbers if x != 20]
    = [100, 300]

    # Loop control flow uses "skip" and "stop"
    for x in my_numbers:
        for y in my_numbers:
            if x == y:
                skip

            # For readability, you can also use postfix conditionals:
            skip if x == y

            if x + y == 60:
                # Skip or stop can specify a loop variable if you want to
                # affect an enclosing loop:
                stop x

    # Tables are efficient hash maps
    table := {"one": 1, "two": 2}
    >> table:get("two")
    = 2

    # You can supply a default argument in case a key isn't found:
    >> table:get("xxx", default=0)
    = 0

    # Otherwise, a runtime error will be raised:
    # >> table:get("xxx")

    # Empty tables require specifying the key and value types:
    empty_table := {:Text:Int}

    # Tables can be iterated over either by key or key,value:
    for key in table:
        pass

    for key, value in table:
        pass

    # Tables also have ".keys" and ".values" fields to explicitly access the
    # array of keys or values in the table.
    >> table.keys
    = ["one", "two"]
    >> table.values
    = [1, 2]

    # Tables can have a fallback table that's used as a fallback when the key
    # isn't found in the table itself:
    table2 := {"three": 3; fallback=table}
    >> table2:get("two")
    = 2
    >> table2:get("three")
    = 3

    # Tables can also be created with comprehension loops:
    >> {x:10*x for x in 5}
    = {1:10, 2:20, 3:30, 4:40, 5:50}

    # If no default is provided and a missing key is looked up, the program
    # will print an error message and halt.

    # Any types can be used in tables, for example, a table mapping arrays to
    # strings:
    table3 := {[10, 20]: "one", [30, 40, 50]: "two"}
    >> table3:get([10, 20])
    = "one"

    # Sets are similar to tables, but they represent an unordered collection of
    # unique values:
    set := {10, 20, 30}
    >> set:has(20)
    = yes
    >> set:has(999)
    = no

    # You can do some operations on sets:
    other_set := {30, 40, 50}
    >> set:with(other_set)
    = {10, 20, 30, 40, 50}
    >> set:without(other_set)
    = {10, 20}
    >> set:overlap(other_set)
    = {30}

    # So far, the datastructures that have been discussed are all *immutable*,
    # meaning you can't add, remove, or change their contents. If you want to
    # have mutable data, you need to allocate an area of memory which can hold
    # different values using the "@" operator (think: "(a)llocate").
    my_arr := @[10, 20, 30]
    my_arr[1] = 999
    >> my_arr
    = @[999, 20, 30]

    # To call a method, you must use ":" and the name of the method:
    my_arr:sort()
    >> my_arr
    = @[20, 30, 999]

    # To access the immutable value that resides inside the memory area, you
    # can use the "[]" operator:
    >> my_arr[]
    = [20, 30, 999]

    # You can think of this like taking a photograph of what's at that memory
    # location. Later, a new value might end up there, but the photograph will
    # remain unchanged.
    snapshot := my_arr[]
    my_arr:insert(1000)
    >> my_arr
    = @[20, 30, 999, 1000]
    >> snapshot
    = [20, 30, 999]
    # Internally, this is implemented using copy-on-write, so it's quite
    # efficient.

    # These demos are defined below:
    demo_keyword_args()
    demo_structs()
    demo_enums()
    demo_lambdas()

# Functions must be declared at the top level of a file and must specify the
# types of all of their arguments and return value (if any):
func add(x:Int, y:Int)->Int:
    return x + y

# Default values for arguments can be provided in place of a type (the type is
# inferred from the default value):
func show_both(first:Int, second=0)->Text:
    return "first=$first second=$second"

func demo_keyword_args():
    >> show_both(1, 2)
    = "first=1 second=2"

    # If unspecified, the default argument is used:
    >> show_both(1)
    = "first=1 second=0"

    # Arguments can be specified by name, in any order:
    >> show_both(second=20, 10)
    = "first=10 second=20"

# Here are some different type signatures:
func takes_many_types(
    boolean:Bool,
    integer:Int,
    floating_point_number:Num,
    text_aka_string:Text,
    array_of_ints:[Int],
    table_of_text_to_bools:{Text:Bool},
    pointer_to_mutable_array_of_ints:@[Int],
    optional_int:Int?,
    function_from_int_to_text:func(x:Int)->Text,
):
    pass

# Now let's define our own datastructure, a humble struct:
struct Person(name:Text, age:Int):
    # We can define constants here if we want to:
    max_age := 122

    # Methods are defined here as well:
    func say_age(self:Person):
        say("My age is $self.age")

    # If you want to mutate a value, you must have a mutable pointer:
    func increase_age(self:@Person, amount=1):
        self.age += amount

    # Methods don't have to take a Person as their first argument:
    func get_cool_name()->Text:
        return "Blade"

func demo_structs():
    # Creating a struct:
    alice := Person("Alice", 30)
    >> alice
    = Person(name="Alice", age=30)

    # Accessing fields:
    >> alice.age
    = 30

    # Calling methods:
    alice:say_age()

    # You can call static methods by using the class name and ".":
    >> Person.get_cool_name()
    = "Blade"

    # Comparisons, conversion to text, and hashing are all handled
    # automatically when you create a struct:
    bob := Person("Bob", 30)
    >> alice == bob
    = no

    >> "$alice" == 'Person(name="Alice", age=30)'
    = yes

    table := {alice: "first", bob: "second"}
    >> table:get(alice)
    = "first"


# Now let's look at another feature: enums. Tomo enums are tagged unions, also
# known as "sum types". You enumerate all the different types of values
# something could have, and it's stored internally as a small integer that
# indicates which type it is, and any data you want to associate with it.
enum Shape(
    Point,
    Circle(radius:Num),
    Rectangle(width:Num, height:Num),
):
    # Just like with structs, you define methods and constants inside a level
    # of indentation:
    func get_area(self:Shape)->Num:
        # In order to work with an enum, it's most often handy to use a 'when'
        # statement to get the internal values:
        when self is Point:
            return 0
        is Circle(r):
            return Num.PI * r^2
        is Rectangle(w, h):
            return w * h
        # 'when' statements are checked for exhaustiveness, so the compiler
        # will give an error if you forgot any cases. You can also use 'else:'
        # if you want a fallback to handle other cases.

func demo_enums():
    # Enums are constructed like this:
    my_shape := Shape.Circle(1.0)

    # If an enum type doesn't have any associated data, it is not invoked as a
    # function, but is just a static value:
    other_shape := Shape.Point

    # Similar to structs, enums automatically define comparisons, conversion
    # to text, and hashing:
    >> my_shape == other_shape
    = no

    >> "$my_shape" == "Shape.Circle(radius=1)"
    = yes

    >> {my_shape:"nice"}
    = {Shape.Circle(radius=1):"nice"}

func demo_lambdas():
    # Lambdas, or anonymous functions, can be used like this:
    add_one := func(x:Int): x + 1
    >> add_one(5)
    = 6

    # Lambdas can capture closure values, but only as a snapshot from when the
    # lambda was created:
    n := 10
    add_n := func(x:Int): x + n
    >> add_n(5)
    = 15

    # The lambda's closure won't change when this variable is reassigned:
    n = -999
    >> add_n(5)
    = 15