Intlang tutorial
WORK IN PROGRESS !!!!!
Hello there, this is a small tutorial to get you started with intlang, the idea is that you can try out intlang code on the fly. So I would recommend to putting this Tutorial and the playground side by side in a split screen :) The tutorial is not a complete guide (many things are left out) and also tries to tailor to someone who knows how to program but is not very experienced in functional programming. So if you are a functional programming pro feel free to skip things ;)
let, let rec
The top-level of intlang consists of let <varname> = <expression> or let rec <varname> = <expression>, both bind the expression on the right to the name on the left of the =. The difference is that with let rec the expression may contain uses of the varname ie. recursive functions. Here is an example:
let thebestnumber = 67
let rec fib = \n. if 1 < n then (fib (n-1) + fib (n-2)) else (n==1) end
As you can see thebestnumber is bound to an int and fib is bound to the fibonacci function. The definition of fibonacci is of course recursive (the name fib appears on the right hand side), which means let rec must be used.
main
Hello world??? here it is:
include io
let main = \() (
io.write_ln "hello, intlang"
)
main is just a top-level let binding, but the name main has some special properties. First of all the type of main needs to be unit -> unit. If this sounds confusing to you just think of it like this: main gets no arguments and returns nothing. Additionally main is special since it is "magically" called when ever you run the program.
include and the library
As you can see in the hello world the first line includes input output. io is part of the library code. The library is also just intlang code and io in particular contain many helpful functions to well do input output operations. Function from io can be used with the io. prefix. io.write_ln "hello, intlang" for example is takes a string, puts a line break at the end and output it to the terminal. Here are some useful io functions that are useful:
io.readln_i32: takes ani32(int) and prints it to the terminal (with trailing line break)io.writeln_i32: takesunit(nothing), reads a line containing ani32from the terminalio.readln_i32vec: takes ani32vector and prints it to the terminal (comma separated and with trailing line break)io.writeln_i32vec: takesunit, reads a line containing comma separatedi32's from the terminal
There is more stuff in the library, some of which will be shown later in the tutorial. You can just have a look what is in there in on the stdlib page.
lambdas
Lambdas are the central kind of expression and if you have never heard of them they are basically functions. So lets go over it on an example:
let f = \x. x+1
let res = f 3
\x. x+1 is a function taking an i32, this i32 is then bound to x and the expression x+1 is evaluated and the result returned. f 3 then applies f to 3. So res is 4.
You can also write "multiple" in one like \x y z.. \() is a special lambda that takes unit type.
let (rec) in
let x = <exp> in <body> will bind the evaluation of <exp> to x and evaluate the <body> with the new binding of x. The rec variant just allows <exp> to contain recursive reference to x. So let (rec) in is basically the local equivalent to top-level let.
vectors
So now that we are done with the basics lets to the most interesting type in intlang: vectors. A vector is very similar to a C++ vector or even a C array, it sits contiguous in memory. The main distinction is that intlang vectors are immutable. In other words: while it is possible to change/mutate a vector after its definition in most other languages, intlang does does not allow this. This means that every time a programer wants to get a different version of an existing vector a new vector is created:
include io
let main = \() (
let x = io.readln_i32 () in
let origin = vec[x] in
let changed = vecset[origin,x+1,0] in (
io.writeln_i32 vecget[origin,0];
io.writeln_i32 vecget[changed,0]
)
)
This program expects an i32 from the user, then the vector origin is created containing one element (x). Line 3 uses the vecset vector operation, but instead of changing/mutating the origin vector a new vector is created. The last two line just print the first (and only) element of both vectors. Give it a try in the playground :)
Here is some example code that shows each vector operation in action. Give it a go :)
include io
let main = \() (
--read a comma separated line from terminal as a vector of i32
let v = io.readln_i32vec () in
let vlit = vec[1,2,3] in -- vector just from literals
let n = veclen[v] in -- get vector length
let elm0 = vecget[v, 0] in -- access first element
let vset = vecset[v, 99, 0]in -- new vector based on v but with the frist element 99
let vmk = vecmk[n, n] in -- vector of length n with every value n
let vslice = vecslice[v, n/2, n/4] in -- 3 quarter of v
let vextend = vecextend[v, 11, n] in --new vector based on v but with n new elements appened (all of value 11)
(
io.writeln_i32vec vlit;
io.writeln_i32 n;
io.writeln_i32 elm0;
io.writeln_i32vec vset;
io.writeln_i32vec vmk;
io.writeln_i32vec vslice;
io.writeln_i32vec vextend
)
)
I hope you now feel like you could write some code with vectors, but for reference here is a more detailed and formal description of what the vector operations do:
-
All kinds of indexes, dimensions and offsets need to be of i32 type
-
Vectors are 0-indexed ie. the first element of a vector has index 0
-
All elements of a vector must be of the same type
-
Vectors can have element types: vector, i32 and i8 (might change in the future)
-
<..>denotes not just an expression but rather a comma separated list of expressions -
vec[<litlist>]: create a vector<litlist>must be a comma separated list of literals- Returns a vector of length equal to the number of elements in
<litlist>, with the order of<litlist>preserved and the left most element of<litlist>at index 0
-
veclen[v]: get the length of a vector- returns the length of the vector
vas ani32
- returns the length of the vector
-
vecget[v, <idxlist>]: access a vector<idxlist>must be a comma separated list of indexes- If
<idxlist>is emtpyvis returned - Else
<idxlist>has a left most elementidxthenvecget[v_inner, <idxtail>]is returned, wherev_inneris the value stored at indexidxinvand<idxtail>is<idxlist>without its left most element
-
vecset[v, value, <idxlist>]: create e new vector based on the old one<idxlist>must be a comma separated list of indexes- If
<idxlist>is emtpyvalueis returned - Else
<idxlist>has a left most elementidxthenv'is returned wherev'is equal (in value not physical) tovbut has the value at indexidxset tovecset[v_inner, value, <idxtail>], wherev_inneris the value stored at indexidxinvand<idxtail>is<idxlist>without its left most element.
-
vecmk[lit,<dimlist>]: initialize a vector<dimlist>must be a non empty comma separated list of indexes- If
<dimlist>has a single elementdimthe result is a vector of lengthdimwith valuelitat at every index. - Else
<dimlist>has more than one element with a left most elementdimheadand the rest of the list<dimtail>then the result is a vector of lengthdimheadwithvecmk[lit, <dimtail>]at every index
-
vecslice[v, start, len]: slice a vector- returns vector equal (in value not physical) to a slice/section of
vstarting at indexstartof lengthlen.
- returns vector equal (in value not physical) to a slice/section of
-
vecextend[v, lit, off]: extend / expand a vector- If
offis positive: returns a vector equal (in value not physical) tovwithvecmk[lit, off]appended - If
offis negative: returns a vector equal (in value not physical) tovwithvecmk[lit, -off]prepended
- If
strings
The hello world program already had a string literal in it. In intlang a string literal is just a i8 vector. You can run this example an confirm for yourself:
include io
let main = \() (
io.write_ln "intlang";
io.write_ln vec['i','n','t','l','a','n','g']
)
tuples
When programs become more complicated it becomes important to organize things in a structured way. While other language have much more extensive possibilities compared to intlang, intlang does offer tuples :) Here is a simple example:
include io
let swap = \tup.
let (a,b) = tup in
(b,a)
let main = \() (
let str1 = io.read_ln () in
let str2 = io.read_ln () in
let tup = (str1, str2) in
let swp = swap tup in
let (swp1, swp2) = swp in
(
io.write_ln swp1;
io.write_ln swp2
)
)
The let ( a, b ) = tup in extracts 2 elements form a tuple and (a,b) creates a tuple. All tuples in this example are 2-Ary ie. they have 2 elements. Intlang does support N-Ary tuples.
type system and polymorphism
tail recursive functions
FBIP optimizaion
What even is a functional programming language???
So if you know the answer to this question skip to the next section as this will bore you.
Since you are still here, I guess you have never/only briefly used a functional programming language so let me explain some basics. Most programming languages are imperative programming languages, some examples would be C/C++, Java or Python. Imperative languages center around statements, where each statement can modify some state. Say you have the following C code:
int f(int x) {
int arr[] = {1,2,4,8,16};
int gres = g(arr);
return arr[0];
}
Each statement is separated by a ;. int arr[] = {1,2,4,8,16}; adds the array to the function local state, int gres = g(arr); passes the array to the function g and return arr[0]; returns the first element from the array. Crucially the array might be changed by g and the result of f could differ from 1, depending on g. Note that in many imperative languages it is not that trivial to create such "tricky" behavior (its C after all... ), but in generally it is always possible.
Functional programming languages offer a different approach, they don't center around statements but rather expressions. Of course expressions in functional programming languages are much richer compared to their counter parts in imperative languages. So lets have a look at similar piece of code in intlang:
let f = ( \x. (
let arr = vec[1,2,4,8,16] in (
let gres = g arr in (
vecget[arr,0] ))))
Does not even look that different right. So lets dissect this. I have put lots of parenthesis in, that hopefully make the structure of the code clearer, but non of them are needed. let (top level / global) or let in (local) expressions bind some expression to a variable. So looking at let f = (...) this does bind f to what ever expression is .... In the example this expression starts with a lambda \x.. Lambda expressions functions ie. \x. ... will when some applied to some argument calculate what ever expression ... is with x bound to the argument provided. Then we continue with let arr = vec[1,2,4,8,16] in (...) and guess what this binds the new vector (basically the same as an array) to the variable arr and then calculates what ever ... is. g arr will apply g to arr (calling a function). And finally vecget[arr,0] is simply the "innermost" expression that will fetch the first element of arr and will also be the result when f is called.
Crucially g can not change arr and the result of f is always 1. Again, I want to note, not all functional language guarantee this. For example an array in Ocaml is a mutable type and can just like in C differ after calling g.
If you still feel as confused about what a functional programming language is as before, don't worry. I would have probably felt the same way, would this have been my introduction. I think in general the best way to learn some thing is to try it and then think about it, so lets do this :)