Add learnXinY example file
This commit is contained in:
parent
ed935eb882
commit
863f07d417
297
learnxiny.tm
Normal file
297
learnxiny.tm
Normal file
@ -0,0 +1,297 @@
|
||||
// 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!")
|
||||
|
||||
// Declare an integer variable (types are inferred)
|
||||
my_variable := 123
|
||||
|
||||
// Assign a new value
|
||||
my_variable = 99
|
||||
|
||||
// Floating point numbers are similar, but have a decimal point:
|
||||
my_num := 2.0
|
||||
|
||||
// Strings can use interpolation with curly braces:
|
||||
say("My variable is {my_variable}")
|
||||
|
||||
// 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]
|
||||
|
||||
// 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"
|
||||
|
||||
// Loop control flow uses "skip" and "stop"
|
||||
for num in my_numbers:
|
||||
if num == 20:
|
||||
// You can specify which loop variable you're skipping/stopping if
|
||||
// there is any ambiguity.
|
||||
skip num
|
||||
|
||||
if num == 30:
|
||||
stop
|
||||
|
||||
>> num
|
||||
|
||||
// Tables are efficient hash maps
|
||||
table := {"one": 1, "two": 2}
|
||||
>> table["two"]
|
||||
= 2
|
||||
|
||||
// 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 default values and fallbacks:
|
||||
table2 := {"three": 3; fallback=table; default=0}
|
||||
>> table2["two"]
|
||||
= 2
|
||||
>> table2["three"]
|
||||
= 3
|
||||
>> table2["???"]
|
||||
= 0
|
||||
|
||||
// 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:
|
||||
>> {[10, 20]: "one", [30, 40, 50]: "two"}
|
||||
|
||||
// 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: "@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],
|
||||
maybe_null_pointer_to_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[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
|
||||
|
Loading…
Reference in New Issue
Block a user