#Overview Here are the main features of the Minimacy language. ## Transparency The Minimacy language is an essential component of Minimacy technology, the aim of which is to develop information systems that a human being can understand 100% of the way they work. This transparency can only be achieved by removing all black boxes, including the operating system. The technology itself must be readable, as well as libraries developed by third parties and integrated into the project. Logically, the Minimacy language must be a high-level language, because the effort required to develop a complex application with a low-level language is such that the developer will be led to massively integrate external libraries, which will be as many black boxes. We have observed that human beings are much more at ease when a text, and a fortiori the source code of a function or algorithm, fits on a single screen. This is generally made possible by Minimacy's non-verbose syntax. This high-level language must itself be transparent, i.e. the developer must be able to make the link between the high-level code and the low-level calculation that will be performed by the microprocessor. The Minimacy language is compiled into a bytecode which is then interpreted by a virtual processor written in C. This bytecode then calls up micro-functions also written in C. With Minimacy, the developer has easy access to the bytecode produced by the compiler, and from there he can easily find the micro-functions called, whose code usually fits on a screen and matches very closely the calculation performed by the microprocessor. Any resulting loss of performance is fully accepted: - the goal of transparency fully justifies it - it is not inevitable: a more efficient algorithm may exist, and Minimacy greatly simplifies the writing of algorithms that would be tedious to write using other languages - Minimacy technology is designed to allow developers to add their own micro-functions in C, optimizing simple but intensive computation. ## A « pragmatic » functional language Minimacy is largely inspired by functional languages and lambda calculus. A program is first considered as a sequence of calculations, and these calculations are grouped into functions. As in mathematics, a function can take a variable number of arguments and always returns a result. fun f(x) = x+1 ;; Each function can call other functions, and can even call itself: this is known as a recursive function. fun factorial(n) = if n<=1 then n else n * factorial(n-1);; The link between functions and calculation is reflected in the synonymy of the following terms: evaluation, calculation, call, execution. In all cases, the aim is to calculate the value returned by a function from its arguments. Functions perform calculations on values that can be numbers, strings, lists, arrays, ... and even functions. A function can produce and return a new function. Minimacy is called "pragmatic" because it accepts certain infringements of the concept of a pure functional language. Mainly, it's possible to define variables whose values can then be modified. This is necessary for the implementation of certain algorithms, and simplifies or accelerates the implementation of others. However, Minimacy's syntax makes this possibility clearly visible. For example, the factorial function described above can be written as follows: fun factorial(n) = if n<=1 then n else let 1 -> result in ( for i=2 ; i<=n do set result = i*result; result );; ## Strong static typing with type inference Minimacy typing has three characteristics: - strong typing: the type of data and functions manipulated is precisely defined, and operations are always performed within a precise framework. For example, you can't add a number to a string. - static typing: types are controlled at compile time. Program execution only starts if typing is correct, including in functions that will rarely, if ever, be evaluated. - type inference: the compiler identifies types itself. With the exception of parametric types, there's no need to write types in the program source code. This simplifies syntax and improves readability. Typing guarantees safe execution: the classic errors of accessing forbidden memory or memory overflow are impossible to write. ## Memory management Minimacy's memory management is automated, with a Garbage Collector mechanism. The system automatically detects memory blocks that are no longer in use and deallocates them, thus avoiding memory leaks. This mechanism also applies to cyclic graphs. The program does not need to deallocate memory blocks it "believes" are no longer needed, which often requires complex mechanisms. Over and above the security of execution, this automatic management makes it easier for the programmer to implement sophisticated algorithms. Minimacy's GC is incremental: it's done continuously, so the program doesn't normally have to be paused to clean up memory. ## Evaluation strategy Even if a Minimacy program is mainly made up of a list of function declarations, which might sound like a list of mathematical definitions, it's important to remember that these functions are only there to organize the sequential progress of a calculation. Understanding the order in which the calculation takes place is therefore of the utmost importance. So, when evaluating a function : - the arguments are calculated first, in the order in which they appear in the source code - then the function is called Consider the following example: f(1+x, g(2)) Calculation takes place in the following order: - the expression 1+x is calculated first - then the function g is called with argument 2 - finally, the function f is called with its two arguments Arguments are always calculated, even if the function doesn't actually use them. There are a few common exceptions, notably the "if ... then ... else ..." construction: the condition is calculated first, and depending on its result, only one of the expressions following the "then" and "else" is calculated. ## Readability The Minimacy language is designed to enable you to write readable programs: - the functional approach with type inference clarifies and reduces writing, so that functions can fit on a single screen - by focusing on calculation rather than modeling, the language enables sequential, almost narrative reading, avoiding the need to jump from one file to the next - naming rules distinguish between functions, which start with a lowercase letter, and global variables and constants, which start with a capital letter - names beginning with an underscore are not exported Explicit typing in languages such as C helps to make code more readable: you can immediately see whether a variable is an integer or a pointer. Type inference would remove this help. However, this observation needs to be tempered: - explicit typing is primarily intended for the compiler - type names are not so clear-cut: in C, a "long" is sometimes 32-bit, sometimes 64-bit - more precise types are added: int8, int8_t, int16_t, uintptr_t, ... o you need to understand the difference between these types o this introduces synonyms that raise questions: char or int8? short or int16_t? o one wonders why the programmer chose one over the other: real functional difference, memory "optimization", random choice? - there are also the types defined for the program, via typedef or struct. What applies to the C language also applies to other languages with explicit typing. In the absence of explicit typing, readability is achieved through naming. For example: fun listLength(p) The name "listLength" leaves no doubt that "p" is a list and that the function returns an integer. #Basic types Minimacy defines a few types natively. {header Type Minimacy syntax Description Bool true, false The boolean type has two values: true and false Int 1234, 0xffc0000 32 bits signed integer Str "hello" The string is a immutable array of bytes Float 1.234 64 bits floating number (32 bits on 32 bits hardware) BigNum - A big signed integer (up to 16384 bits) Bytes - A mutable array of bytes. Only the content is mutable, not the size Buffer - A buffer of bytes, with mutable content and mutable size } For every type there is a special value, [[nil]], which means “empty” or “undefined”. The use of this value simplifies program writing (as we'll see later with the "if ... then ... else ..." construction) and can replace the somewhat cumbersome use of "try ... catch ..." to handle simple cases. This [[nil]] value also applies to numbers and Booleans: { Int The value [[nil]] in an arithmetic expression will be considered as 0 Float The value [[nil]] in an arithmetic expression will be considered as 0.0 Bool The value [[nil]] in an arithmetic expression will be considered as [[false]] } \ 1 + nil >-> 1 if nil then 1 else 2 >-> 2 #Usual operators {header Operator Type Description + - * / ** % fun Int Int -> Int arithmetic on integers (**: power) & | ^ << >> fun Int Int -> Int operations on bits +. -. *. /. **. fun Float Float -> Float operations on floats (**.: power) && || fun Bool Bool -> Bool operations on booleans ! fun Bool -> Bool operation on booleans < > <= >= fun Int Int -> Bool comparison on integers <. >. <=. >=. fun Float Float -> Bool comparison on floats == != <> fun a1 a1 -> Bool comparison on any type (<> and != are the same) } #Functions Minimacy loves functions. They are the most important declaration. A function takes any number of arguments and returns exactly one value. Function names must start with a lowercase letter. The syntax is designed to keep it simple, close to the mathematical usual syntax. Consider the simple function “f(x) = x+1”. In Minimacy you simply define f this way: fun f(x) = x+1 ;; Observe that in Minimacy, all declarations end with a double semicolon “;;”. Calling f with argument 2: f(2) With two arguments the function “g(x,y) = x+y” is: fun g(x, y) = x+y ;; Calling f with arguments 2 and 3: g(2, 3) Combining f and g to call “f(g(2,3))”: f(g(2, 3)) There is a special syntax to chain function calls on the last argument, and to prevent situations with a lot of closing parentheses ')'. g(1,f(2)) May be written like this: g(1,*) f(2) Another example: g(1,g(2,g(3,g(4,g(5,6))))) Becomes more readable when written like this: g(1,*) g(2,*) g(3,*) g(4,*) g(5,*) 6 This syntax is only possible for the last argument: '*' is always the last argument, followed by ')'. #Recursive functions A function may call itself: this is known as a recursive function. fun factorial(n) = if n<=1 then n else n * factorial(n-1);; When n is less or equal to 1, the function returns n. When n is more than 1, it performs the following computation: - call factorial(n-1) - multiply the result by n and return the result (observe that the recursive call is not the last operation) It is easy to understand why this program will finish. At each recursion the value of the argument decreases: n, n-1, n-2, ... At some point it will reach 1 which returns directly with no recursive call. If we follow the calculation, we understand that the system will perform the multiplication in this order: *2, *3, *4, ... *n-1, *n: before we can multiply by n we need the result of “factorial(n-1)”, and so on. This means the call stack will reach a depth of n, which could be too much when n is big. This is a common issue with all recursive languages. We can transform our program this way: fun _factorialLoop(n, result)= if n<=1 then result else _factorialLoop(n-1, n*result);; fun factorial(n) = if n<=1 then n else _factorialLoop(n, 1);; Now the recursive function is _factorialLoop. When n is more than 1, it performs the following computation: - compute n-1 - compute n*result - call _factorialLoop with these two values. We observe that the recursive call is now the last operation. The Minimacy compiler detects this configuration called “Terminal function call” and prevents the call stack from growing. #Echo, dump, dumpCallstack There are several functions to display stuff on the console (aka standard output, or serial output). This may be regular application output, but developers can use them to debug their program. The following functions can be inserted easily as they return their argument unchanged. { [[echo]] fun a1 -> a1 display the argument [[echoLn]] fun a1 -> a1 display the argument and a newline [[echoTime]] fun a1 -> a1 display the time since the launch, then the argument [[dump]] fun a1 -> a1 display detailed information about the argument [[hexDump]] fun Str -> Str display a hexadecimal dump of the argument } There is no need to add parenthesis around the argument of these functions. echo "Foobar" echo("Foobar") [[dumpCallstack]] displays the callstack content. [[dump]] may be used to display functions bytecodes. fun f(x) = x+1;; fun main() = dump #f;; (the meaning of “#” before f is explained in the [Lambda functions](link://hash_functions) paragraph) >-> fun f > bytecode: >| args=1 locals=0 >| 0 rloc.b 0 >| 2 int.b 1 >| 4 add >| 5 ret We’ll see later how to read the bytecode. We can already say that it is based on a stack. This bytecode pushes the argument, then pushes the integer value 1, then pulls two values, computes the sum, and pushes the result, then returns. #Local variables When your calculation gets more complex or when there is redundancy, you may define variables to store intermediate results. Let’s consider the following example: fun squareSum(x, y) = let x*x -> square_x in let y*y -> square_y in square_x + square_y;; The line “let x*x -> square_x in ...” means: I compute x*x and store the result in a fresh variable square_x that I can use in the following expression. You may be surprised by the order of the elements in the “let” declaration, as many languages would say “let square_x=x*x”. The rationale behind this is that it sticks better with the way the computer works: - first we compute a value: let x*x - then we decide to assign a name to this value: -> square_x - then comes the computation in which we will use this name: in ... square_x... It is important to understand the scope of these local variables. They are defined only in the expression following the “in”. The following sample will fail: let 1 -> x in echoLn x; x Here the x variable can be used after the echoLn, but the semicolon marks the end of the expression. Then the second line “x” fails because x is not defined anymore. A way to fix it is to group several computations in a single expression with parenthesis and semicolons: let 1 -> x in ( echoLn x; x ) When grouping several expressions inside parentheses to get a single expression: - internal expressions are computed - their resulting value is ignored except for the last one - the value of the group of expressions is that of the last expression inside the parentheses It is also possible to redefine a local variable twice or more: let 10 ->x in let x*x -> x in echoLn x This program will display 100, as the echoLn call is in the expression following the “in” of the second definition of x. However, the first definition of x is not lost. The following program will display 100 then 10: let 10 -> x in ( let x*x -> x in echoLn x; echoLn x ) The second echoLn is not in the scope of the second definition of x, which ends with the semicolon. Therefore the second echoLn displays the value of the first definition of x. Once a local variable is defined with a let, it is possible to change its value: let 1 -> x in ( set x=x+1; echoLn x ) To help the readability of your program, it is advised to break down your calculation into several steps, one per line, the result of each being stored in a new local variable, as we did on the squareSum function above. This way you can almost narrate your program as if it was a story. # Lambda functions In Minimacy you can manipulate functions like any other data: we use “lambda functions”. Lambda functions are functions with no name which are “created” dynamically, for example inside the code of a function. Consider this example: fun makeConstantAddition(n)= lambda (x) = x+n;; The “makeConstantAddition” function takes one argument “n” and returns a lambda function. This lambda function takes one argument “x” and returns the sum x+n. To call a lambda function we use “call”: let makeConstantAddition(10) -> f10 in call f10 (123) In this example we first create a lambda function “f10” which will add 10 to its argument. Then we call “f10” on 123. The result will be 133 (=123+10) It is also possible to get a lambda function from a declared function. Just add a hash sign in front of the function name:[hash_functions](target) let #echoLn -> fEchoLn in call fEchoLn (123) #Constants and global variables It is possible to define constants and global variables in a program. const X=1;; var Y=2;; Their name must start with a capital letter. Observe that in Minimacy, all declarations end with a double semicolon “;;”. It is possible to declare a global variable without its value: var X;; It is then initialized with [[nil]]. In Minimacy programs, the order of declarations is irrelevant except for the initial values of constants and global variables, as they are computed in the order of declarations. const X=1;; var Y= X*2;; In this case, Y is 2. var Y= X*2;; const X=1;; In this case, Y is 0, because X has not yet been calculated and is therefore [[nil]], and [[nil]] is considered as 0 in arithmetic operations such as addition. There is another way to declare integer constants, where you enumerate from 0: enum A, B, C;; Then A=0, B=1, C=2 #Data structures ##List Lists are very popular in functional programming: they are an immutable ordered set of values of the same type. [[nil]] is the empty list. nil We use the double-colon “::” to add elements. The following creates a list with one single element, the integer 1: 1::nil The syntax for the list containing 1, 2, 3 in this order: 1::2::3::nil We could add parentheses like this: 1::(2::(3::nil)) The colon character assumes that the left side is an element and the right side is a list. Two useful functions are [[head]] and [[tail]]: - [[head]] returns the first element of the list - [[tail]] returns the list after the first element head(1::2::3::nil) >1 tail(1::2::3::nil) >2::3::nil Another way to access inside a list is using local variables: let 1::2::3::nil -> a::next in ... In this example, “a” is 1, and “next” is 2::3::nil let 1::2::3::nil -> a::b::next in ... In this example, “a” is 1, “b” is 2, and “next” is 3::nil When you don’t need a value, there is no need to invent a label, just use underscore “_”: let 1::2::3::nil -> a::_ in ... let 1::2::3::nil -> a::_::next in ... The same mechanism applies to functions arguments: fun foobar(a::next) = ... This defines a “foobar” function whose first argument is a list. “a” is its first element, “next” is the list after the first element. Lists are often used in recursive functions. The following example shows a simple way to compute the number of elements in a list. fun listLength(p) = if p==nil then 0 else 1+listLength(tail(p));; ##Array Arrays are a set of elements of the same type which can be accessible with their index. It is not possible to change the size of an array after its creation, but it is possible to set its values. An array can be defined statically or dynamically: var MyStaticArray={ 1, 2, 3 };; var MyDynamicArray= let arrayCreate(3, nil) -> a in ( set a[0]=1; set a[1]=2; set a[2]=3; a );; Both MyStaticArray and MyDynamicArray have the same content. Remember that the “let” defines local variables (here “a”) which you can use only in the expression which follows the “in”. That’s why we add parentheses to chain three “set” functions. The value of this expression between parentheses is the value of its final expression, after the last “set” (here “a”). If you don't need to change values or access an element by its index, you should consider using lists instead, as lists are more in the vein of functional programming. ##Fifo Fifo means “first in first out”. It is like a pipe: you push elements from one end, and you pull them from the other end. All these elements must be the same type. A fifo of integers will have the type “fifo Int”. The 3 basic functions for fifos are: { [[fifoCreate]] create a fifo [[fifoIn]] fifo value push the value in the fifo [[fifoOut]] fifo pull the next value from the fifo } There are other functions you can find on [this reference page.](page://ref..Fifos) let fifoCreate() -> fifo in ( fifoIn(fifo, 1); fifoIn(fifo, 2); fifoOut(fifo) ) >1 The result of this whole expression is the result of the last part “fifoOut(fifo)”. It is 1 because 1 was the first element to be put in. ##Hashset Hashsets are unordered sets containing elements of the same type. They are based on hash tables so that their operations are considered to run in constant time. A hash table is an array of lists of elements. Given an element we apply a hash function to this element to get an index in the array. To add an element to a hash table, the system uses this index to determine where to store the element in the array. The element is added to the list found in this position. The same mechanism is used to determine whether a hash table contains an element. If the hash table is large enough, each list from the array should be very small, making it fast to add/remove/find an element. The 4 basic functions for hashsets are: { [[hashsetCreate]] bitSize create a hashset [[hashsetAdd]] hashset value add the value to the hashset [[hashsetRemove]] hashset value remove the value from the hashset [[hashsetContains]] hashset value determine whether the hashset contains the value } There are other functions you can find on [this reference page.](page://ref..Hashsets) In Minimacy, the hash table size is always a power of 2: 2, 4, 8, 16, ... When creating a hashset with hashsetCreate, the “bitSize” argument is this power: {header bitSize hash table size 4 16 8 256 16 65536 } When bitSize is 8, the hash table size is 256. This means that the average length of the internal lists will be 1/256 of the number of elements in the hash table. Therefore, the search for an element will be around 256 times faster than when using a single list to store all the elements. ##Hashmap Hashmaps are unordered sets associating a key with a value. They are based on hash tables so that their operations are considered to run in constant time. See Hashsets above for a description of hash tables. The 3 basic functions for hashmaps are: { [[hashmapCreate]] bitSize create a hashmap [[hashmapSet]] hashmap key value add the pair (key, value) to the hashmap [[hashmapGet]] hashmap key returns the value associated to the key } There are other functions you can find on [this reference page.](page://ref..Hashmaps) [[hashmapGet]] returns [[nil]] when the key is not in the hashmap. Hashmap don’t store [[nil]] values: to remove a key from the hashmap, use “hashmapSet hashmap key nil” let hashmapCreate(4) -> h in ( hashmapSet(h, 123, "abc"); hashmapSet(h, 456, "def"); hashmapGet(h, 123) ) >abc ##Tuple Tuples are a set of values of mixed types, you don’t need to declare it. Just use squared brackets. let [1, 2, "foobar"] -> myTuple in let myTuple -> [a, b, c] in ... The “let” allows you to extract the inner values of the tuple. Tuples are not mutable: you can’t change their inner values. ##Structures Structures are like tuples but their values are named (we call them “fields”) and it is possible to change the values of the fields. struct Label=[xL, yL, nameL];; const Origin= [xL=0, yL=0, nameL="origin"];; fun vector p q = let q.xL - p.xL -> dx in let q.yL - p.yL -> dy in [dx, dy];; (observe that in Minimacy, all declarations end with a double semicolon “;;”) We define a structure with the list of its fields. These fields are considered as functions and therefore must have distinct names. This means that in a single mcy file, you cannot define two structures with the same field name ‘x’. We recommend using the initial of the structure name as a final capital letter to the field: - "Label" starts with “L” - we add “L” at the end of its field names: xL, yL, nameL We instantiate a structure like this: [xL=1 ] It is not necessary to give a value to all fields, the missing fields will be [[nil]]. You may also instantiate a structure without any value: [Label] The “vector” function above shows how to access the fields. ## Derived structures Sometimes we’d like to define a new structure by adding a few fields to an existing one. Consider the following example: struct Point=[xP, yP];; struct NamedPoint= Point+[nameP];; struct Vector=[dxV, dyV];; fun vectorFromTwoPoints(p, q) = [ dxV = q.xP-p.xP, dyV = q.yP-p.yP ];; We first define a structure Point with 2 coordinates xP and yP. Then we define a structure NamedPoint by adding a field nameP to the structure Point. We define the function “vectorFromTwoPoints” which uses the fields xP and yP: now we can call it on values of type Point or NamedPoint. It is possible to safely cast derived structures. Our example with Point and NamedPoint leads to this: fun castFromNamedPointToPoint(p) = Point< p;; fun castFromPointToNamedPoint(p) = NamedPoint x, addN x y -> nodeEval(x)+nodeEval(y), mulN x y -> nodeEval(x)*nodeEval(y), zeroN -> 0, _ -> 0;; The function “nodeEval” illustrates how to deal with a sum. The construction “match ... with ...” makes it possible to extract the constructors and the associated values. The last line handles the default case with “_ -> ...”. Minimacy uses a comma to separate the different cases. The values associated to each case must have all the same type, because they are the result of the function “nodeEval”, and a function returns only one type. The [[match]] has more usages, that we’ll see in a dedicated chapter. It is possible to extend an existing sum with new constructors. For example, we could add the Node sum like this: extend Node with divM _ _, negM _;; #Execution controls ##if ... then ... else ... This the classical construction we can see in almost every programming language. However, it should be noted that the “if...then...else...” construction is considered as a function and returns a value. Therefore, it could also be compared to the C ternary operator “?:”. fun strSign(x)= if x>=0 then "+" else "-";; The condition is evaluated. It must be a boolean. Depending on its result the expression following “then” or the expression following “else” is evaluated and returned. Both expressions must be the same type. As seen previously when we need several expressions instead of one, we use parentheses and semi-colons: fun isPositive x= if x>=0 then ( echoLn "x is positive"; true ) else ( echoLn "x is negative"; false );; The “else” may be omitted for clarity, the compiler assumes it would be “else [[nil]]”. When chaining if ... then ... else if ... then ... else ..., it is possible to use 'elif': if ... then ... elif ... then ... else ... It might be cumbersome to get the same type for the expressions 'then' and 'else' when you don't use the result of the 'if' construction (which is the sign you are not doing functional programming). Then you may insert 'void' before the 'if': the compiler will ignore the values after 'then' and 'else', and the 'if' construction will return [[nil]]. ##match We already saw how the “match” is used to extract the constructors from a sum: fun nodeEval(node)= match node with intN x -> x, addN x y -> nodeEval(x)+nodeEval(y), mulN x y -> nodeEval(x)*nodeEval(y), zeroN -> 0, _ -> 0;; Each case handled by the match must return the same type. Here it is always “Int” (integers). It is possible to use “match” for any kind of data. For example with our previous enum: enum A, B, C;; fun echoMyEnum(val)= echoLn match val with A -> "A", B -> "B", C -> "C", _ -> "?";; The “match” can be used to extract a derived type: struct Point=[xP, yP];; struct NamedPoint= Point+[nameP];; fun getName(p) = match p with NamedPoint q -> q.nameP, Point q -> "no name";; It might be cumbersome to get the same type for each case when you don't use the result of the 'match' construction (which is the sign you are not doing functional programming). Then you may insert 'void' before the 'match': the compiler will ignore the values of each case, and the 'match' construction will return [[nil]]. ##for “for” is a way to loop in the evaluation of an expression as long as a condition is fulfilled. The value returned by “for” is the value of the expression in the last loop or [[nil]] if no loop occurred. Most of the time we don’t care about this returned value: “for” comes from imperative programming. There are different kinds of “for” depending on the kind of iterator. for i=0;i<10 do echoLn i A local variable “i” is created with the value 0. Then, as long as i<10 the expression is evaluated and then the iterator is incremented. It is possible to define another increment: for i=0;i<10;i+2 do echoLn i At the end of each loop we will replace the value of “i” by “i+2”. The iterator may be structured: for [a, b]=[1, 2];a<10;[a+1, b] do echoLn b \ “for” may apply on a list: for val in 3::2::1::nil do echoLn val The local variable “val” will be 3 in the first loop, 2 in the second, 1 in the last. \ “for” may apply on an array: for val of {3, 2, 1} do echoLn val The local variable “val” will be 3 in the first loop, 2 in the second, 1 in the last. It is possible to get the corresponding index: for val,i of {3, 2, 1} do echoLn [i, ": ", val] >0: 3 >1: 2 >2: 1 \ As with function arguments and local variables, the iterator may be structured: for [key, val] in [1, "a"]::[2, "b"]::[3, "c"]::nil do ( echo key; echo ": "; echoLn val ) As with “let ... -> ... in ...” the iterator variables are defined only in the expression following “do”. Use parentheses when you need more expressions like in the above example. ##while “while” is a simple loop construction: an expression is evaluated as long as a condition is fulfilled. let 10 -> val in while val>0 do set val=val-1 The value returned by “while” is the value of the expression in the last loop, nil if no loop occurred. Most of the time we don’t care about this returned value: “while” comes from imperative programming. ##break It is possible to leave a “for” loop or a “while” loop with the “break”. It must be followed by the value that will be returned by the “for” or the “while”. for val in myList do if val == 123 then break true else nil; This function iterates through the list and breaks the loop with value true if 123 is in the list. Else the loop will go through all the list and then return [[nil]]. ##try...else Exceptions are a way to abort the normal processing of your functions. You may throw an exception, and this will resume the execution from a previous “try ... else”. Minimacy differentiates Exceptions and Errors: Exceptions abort the evaluation and Errors are simply information attached to Threads. The only way to throw an exception is to call 'abort'. In the following example we detect a division by zero and when it happens we display a message and return -1: fun myDiv(x, y)= try if y==0 then abort else x/y else (echoLn "division by zero detected"; -1) 'try' is followed by an expression. When the evaluation of this expression leads to an 'abort', the evaluation stops and instead the expression after the 'else' is evaluated. The 'abort' may be in this expression or in a function called by this expression. When the 'else' is missing, it is considered as if there was 'else nil'. A lot of programming languages terminate the thread when an exception is thrown outside a “try ... catch” (aka an uncaught exception). This leads to unpredictable behavior when you call a function from a 3rd party library: may this function throw an exception or not? Then you may be encouraged to add 'try' keywords everywhere, especially where it is useless. In Minimacy if 'abort' is encountered outside a 'try' (there is no try in the call stack), it does not stop the thread and is simply handled as 'return nil'. Therefore you are encouraged to use exception for the own usage of your package, for example when parsing a string with a specific syntax. You may have a look to [core.util.json](page://ref.core.util.json) package. ##Errors Minimacy defines the type “Error”. It is a sum. The bios defines the constructor “msgError” with a string argument. You may define your own error constructors by extending the type Error. For example: extend Error with myFirstError _, myConstantError;; Then your program may attach an error to the current thread, for example: setError(msgError, "there is an error") And your program may retrieve the attached error with: lastError() Calling lastError will clear the error. A subsequent call to lastError will return nil. > setError(msgError, "there is an error!") >-> Error: constructor msgError > 0 : > Str: "there is an error!" > lastError() >-> Error: constructor msgError > 0 : > Str: "there is an error!" > lastError() >-> Error: nil ##return It is possible to leave a function with the “return”. It must be followed by the value that will be returned by the function. fun contains(myList, searchedValue) = for val in myList do if val == searchedValue then return true; false ;; This function iterates through the list and stops the loop and returns true when it finds an element with the searched value. Else, after the iteration is complete, it returns false. #Typechecking ##Inference Minimacy includes a strong static type checking with type inference. It means that Minimacy infers all the types as it compiles the program and doesn’t allow any mismatch. When you launch your program, the console displays the inferred types. Let’s create a file « hello.mcy » with the following content: fun f(x) = x+1;; When we launch it, the console displays: >>>>>>>>>> package: hello >> fun f : fun Int -> Int We can see that “f” has been detected as a function which takes one Int and returns one Int. It is easy to edit “hello.mcy” and try new things. When writing program you will encounter compiling errors. Some of them are about wrong syntax, some of them are type checking errors. Let’s focus on these errors. fun f(x) = x + "abc";; >Compiler: 'Str' does not match with 'Int' >Provisional inferred types: > local x: Int >Compiler: error compiling function 'f' > >> pkg hello: >> line 1: >> fun f(x) = x + "abc";; >> ^ There is an error because addition needs two integers and we provide a string as the second argument: - the error message mentions: 'Str' does not match with 'Int' - it mentions the types already inferred: x is considered to be an integer because it is the addition first argument Let’s have a look at other kinds of type inference. fun f(x, y) = x+y ;; >> fun f : fun Int Int -> Int When there are more than one argument their types are written after “fun” and before the arrow “->” which is followed by the type of the result. fun f(x) = x;; >> fun f : fun a1 -> a1 “f” takes one argument and returns it unchanged. This means that “f” may work on any type of argument. This is what we call polymorphism in functional programming. Then, the compiler names it “a1”: - “a” stands for “any” - “1” because it is the first type “any” in this expression fun f(x, y) = x==y ;; >> fun f : fun a1 a1 -> Bool Here “f” takes two arguments and compares them. They can be any type, but they must have the same type: that’s why the compiler infers “a1” for both x and y. fun f(x, y) = [x y] ;; >>fun f : fun a1 a2 -> [a1 a2] Here “f” takes two arguments and returns a tuple containing these arguments. They can be any type and they don’t need to be the same type: that’s why the compiler infers “a1” for x and “a2” for y. ##Data structures Let’s have a look at the types of data structures we presented. A list of integers: list Int An array of integers: array Int A fifo of integers: fifo Int A hashset of integers: hashset Int A hashmap of integer keys and string values: hashmap Int -> Str \ A structure: struct Label=[xL, yL, nameL];; const Origin= [xL=0, yL=0, nameL="origin"];; >> struct Label : [ nameL yL xL ] >> field xL : Label -> Int >> field yL : Label -> Int >> field nameL : Label -> Str >> const Origin : Label This defines a new type “Label” and fields are noted “StructureType -> FieldType”. \ A derived structure: struct Point=[xP, yP];; struct NamedPoint= Point+[nameP];; const Origin= [xP=0, yP=0, nameP="origin"];; >> struct Point : [ yP xP ] >> field xP : a1{Point} -> Int >> field yP : a1{Point} -> Int >> struct NamedPoint : Point + [ nameP ] >> field nameP : NamedPoint -> Str >> const Origin : NamedPoint Here we can see the real type of field “xP”: a1{Point} -> Int This means that the field applies to “any” structure derived from “Point”, including “Point” itself. ##Weak types Constants, global variables, fields and constructors can’t be polymorphic: their type must be precise. What happens when the compiler can’t infer their type? var X;; >> var X : w1 >> >Compiler: weak type error >weak type hello.X: w1 The global variable “X” has no initial value. The compiler gives it the type “w1”: - “w” stands for “weak” - “1” because it is the first encountered weak type Weak types should be temporary during the compiling. If a type remains weak at the end of the compiling, it fires a compiling error “weak type error” as above. var X;; fun f(a) = a+X;; >> var X : Int >> fun f : fun Int -> Int This time the “f” function tells the compiler that X is an integer because it uses its value in an addition. X’s type is no longer a weak type, there is no compiling error. ##Parametric types Fields and constructors can’t be polymorphic, but they can be parametric. Consider the following example: struct Ref=[valRef];; fun refCreate(x)=[valRef=x];; fun refGet(ref) = ref.valRef;; fun refSet(ref, val)= set ref.valRef=val;; fun main()= refCreate(123);; We define a structure “Ref” with only one field “valRef”. It works like a memory cell: we create it with an initial value (refCreate), then we can access the value (refGet) or set the value (refSet). This is a structure we would like to use with any type of value. However, the last line (refCreate(123)) will inform the compiler that valRef is an integer, and we won’t be able to use it with strings or any other type. What we need is a parametric type. We just change the first line: struct Ref{A}=[valRef:A];; fun refCreate(x)=[valRef=x];; fun refGet(ref) = ref.valRef;; fun refSet(ref, val)= set ref.valRef=val;; var X=refCreate(1);; var Y=refCreate(1::2::3::nil);; >> struct Ref{a1} : [ valRef ] >> field valRef : Ref{a1} -> a1 >> fun refCreate : fun a1 -> Ref{a1} >> fun refGet : fun Ref{a1} -> a1 >> fun refSet : fun Ref{a1} a1 -> a1 >> var X : Ref{Int} >> var Y : Ref{list Int} From now on, Ref is a parametric structure and may be used on any type: - X is a Ref containing an integer - Y is a Ref containing a list of integers You may have multiple parameter: struct Multi{A B}=[aM:A, bM:B];; >> struct Multi{a1 a2} : [ bM aM ] >> field aM : Multi{a1 a2} -> a1 >> field bM : Multi{a1 a2} -> a2 Parametric types are also available for sums. For example, a binary tree of any type of value could be defined by: sum Tree{A}= nodeT left:Tree{A} value:A right:Tree{A}, emptyT;; >> sum Tree{a1} : emptyT, nodeT >> constr nodeT : fun Tree{a1} a1 Tree{a1} -> Tree{a1} >> constr emptyT : Tree{a1} ##Explicit types Even if it is not necessary, it is possible to write types in the source code. It could be useful in various situations: - prevent weak types - increase readability - helps the writing by checking the type at some point of a calculation We use the 'colon' (:) to start writing a type. When we introduce a new name in a declaration, we may append its type just after it: fun f:Int(x:Int) = x+1;; Here we have specified the type of “f” and “x”. This is also possible when we introduce a new local variable. Inside the computation, we can write an explicit type before an expression. The compiler will check that this explicit type is compatible with the expression’s type. fun f(x) = :Int x+1;; When the type is not atomic like Int, Str, ..., parentheses are required: fun f(x) = :(list Int) x:(x+1):nil ;; #Packages A Minimacy program is a *.mcy file containing a list of declarations: functions, constants, global variables, structures, sums. All declarations end with a double semicolon “;;” A Minimacy file is considered as a “package”, and its name is the name of the file without the “.mcy” extension. There are two situations where a single file is not enough: - the file is getting too large to be readable (too many lines to scroll) - the file is dealing with computations which could be used by other programs ##include In the first case, we should split the file and use [[include]]. You can only include derived file names: the name of the package followed by “._” (period underscore) and what you need after. For example the hello.mcy file could contain: include hello._firstPart;; include hello._secondPart;; From the compiler point of view it is exactly the same as a single large file. ##use In the second case, we should isolate these computations in a separate package (aka a library), and import it to use its functions. For example our hello.mcy file is: fun factorial(n) = if n<=1 then n else n * factorial(n-1);; fun main() = echoLn factorial(10);; We consider the factorial function could be useful for other programs. We create a new lib.factorial.mcy file, containing: fun factorial(n) = if n<=1 then n else n * factorial(n-1);; Then, we update hello.mcy like this: use lib.factorial;; fun main() = echoLn factorial(10);; If we import several packages with [[use]], there might be more than a “factorial” function. We can specify which one we use like this: use lib.factorial as F;; fun main() = echoLn F.factorial(10);; This ensures that the “factorial” function is from package “factorial”. It is possible to import a package which imports other packages and so on. There is a possibility of loops. In Minimacy, such loops are forbidden and detected by the compiler. ##visibility When we design a package for import, we don’t want every declaration to be visible. Minimacy provides two visibility mechanisms. The implicit mechanism relies on naming: - when a name starts with underscore, it is not visible (aka private) - else it is visible (aka public) fun foo(x) = x+1;; fun _bar(x) = x+2;; >> fun foo : fun Int -> Int >> fun _bar * fun Int -> Int The star ‘*’ in the second line is a reminder for the private nature of the function “_bar”. There is also an explicit mechanism, which provides the opportunity to document your package. It relies on the “export” declaration, which must be at the beginning of the package, before anything else. export foo(x);; fun foo(x) = x+1;; fun bar(x) = x+2;; >> fun foo : fun Int -> Int >> fun bar * fun Int -> Int Names in the export declaration must be exactly the same as in the actual declaration. Else an error is raised by the compiler: export foo(y);; fun foo(x) = x+1;; >Compiler: argument name 'x' does not match with export declaration 'y' >Compiler: error compiling function 'foo' >> pkg hello: >> line 3: >> fun foo(x) = x+1;;