Skip to content

Simple Code Examples

David Jeske edited this page May 22, 2017 · 46 revisions

Let's begin with a simple Hello World. At it's core, Irken is an s-expression language like Scheme, Lisp, and Clojure.

(include "lib/basis.scm")
(define (main)
   (printf "hello world\n"))
(main)

Type Inference

Because of type-inference, types may usually be omitted, making Irken feel somewhat like dynamic languages such as Scheme, Python, Ruby, or Javascript. However, unlike those languages, the compiler determines all types statically at compile-time, both to report errors, and produce highly optimized code. Types may also be stated explicitly if you prefer.

(define (addone x) 
    (+ x 1))
(define (addten y) : (int -> int)   ;; explicit type signature
    (+ y 10))
(addone 30)       ;; -> 31
(addten 30)       ;; -> 40
(addone "cow")    ;; -> compile time error

Lists

One of the biggest departures from Scheme is that lists are statically typed and monomorphic, i.e. every element of a list must be of the same type. While this may sound horribly restrictive, in practice it is not.

Semantically, most lists are monomorphic. When dynamically typed programs store elements of different types in a list, they are often representing tuples - a fixed size set of items where position has meaning. In Irken tuples are not stored in lists, but as variants, which we'll cover below.

The type safety guarantees made by this type system make for more robust and programs that can outperform dynamic languages because it needs no runtime type checking. Let's look at some examples of lists.

(length (LIST 1 2 3 4))            ;; -> 4 
(define (double x) (* x 2))        ;; function (int -> int)
(map double (LIST 1 2 3))          ;; -> (2 4 6) : (list int)
(map double (LIST 1 2 "gotcha"))   ;; <compile error>

Classical Lisp operations are available on lists, though it's more idiomatic to use pattern matching, which we'll cover below.

(cons 1 '())                       ;; -> (1)
(car (LIST 4 5 6))                 ;; -> 4

Polymorphic Variants

When we wish to store a tuple in Irken, we store it as a variant. The first type of variant we'll talk about is a Polymorphic Variant. You can think of these as ad-hoc tuples, tagged with a name. All polymorphic variants with the same tag, along the same code path, must have the same type signature. This assures all code that packs and unpacks a particular variant agree on what the variant contains. Unlike lists in Scheme, variants are stored in a packed block of memory, like a C-struct.

For example, if we want to create a 3d-vector (x,y,z) we could create it as a polymorphic variant, like this:

(:vec3d 3 4 -3)

When true heterogeneous lists are required, you may make a list of variants, with each variant carrying unique information.

(define a-list (LIST (:int 1) (:string "two") (:symbol 'three)))

If we try to add another element to this list with a mis-matched variant type, it's a compile error:

(define b-list (cons (:int "one") a-list))  ;; compile error, type (:int int) != (:int string)

Static Variants

When a set of variants are always connected and used-often, it may make sense to provide an explicit datatype declaration, to bound and document the type. This is done with static variants, also known as algebraic datatypes. Here is a static variant declaration, and a list created with those variants: (See Datatypes)

(datatype thing
  (:int int)
  (:str string)
  (:sym symbol)
  )
(LIST (thing:int 1) (thing:str "two") (thing:sym 'three))

Pattern Matching

Pattern matching makes operating on variant datatypes concise and because pattern matching is exhaustive, the compiler will complain if you forget to handle a case. This is true even when we use ad-hoc polymorphic variants. Below is a function to print the contents of our original polymorphic variant list. Here lists are matched with the special form ( head . tail ), see pattern matching for more explanation of the syntax.

(define print-things
  ()                      -> #u  ;; the undefined type, like void
  ((:int x) . tail)  -> (begin (printf (int x) "\n") (print-things tail))
  ((:str s) . tail)  -> (begin (printf s "\n") (print-things tail))
  ((:sym sy) . tail) -> (begin (printf (sym sy) "\n") (print-things tail))
 )

And here is a version which prints a list of our static variant type:

(define print-things
  ()                      -> #u  ;; the undefined type, like void
  ((thing:int x) . tail)  -> (begin (printf (int x) "\n") (print-things tail))
  ((thing:str s) . tail)  -> (begin (printf s "\n") (print-things tail))
  ((thing:sym sy) . tail) -> (begin (printf (sym sy) "\n") (print-things tail))
 )

Structural Typing with Row Polymorphism

One of the more unique things about Irken is Row Polymorphism for records.

(define (fn record)  ;; compiler infers type  : ( {a=int ...} -> int)
   (+ record.a 1))

This means that because this function accesses record.a expecting an int, it is compatible with any record containing an a=int field. In dynamic languages, this is referred to as Duck Typing. Unlike dynamic languages, Irken reports Structural Typing errors at compile time. Consider the following program:

(include "lib/basis.scm")

(define (fa x) 
   (begin 
      (printf x.a) 
      x))

(define (fb x) 
   (begin 
      (printf x.b) 
      x))
(define (fc x) 
   (begin 
      (printf x.c) 
      x))

(fc (fb (fa {a="1" b="2" c="3"}))) ;; -> prints "123"
; (fc (fb (fa {a="1" b="2"}))) ; << compiler error, because field 'c' is missing

Obviously you can write this program in any dynamic language. However, in Irken this program is statically typed. If you remove one of the fields from the record creation, it's a compiler error. If you try to access a field that isn't present in the record, it's a compiler error.

Exceptions

Irken supports exceptions, which behave as you might expect from other languages. We use a syntax closer to Python than Scheme.

(include "lib/basis.scm")

(define (a)
   (raise (:mySillyException "yo"))
   1)

 (try
     (a)
  except
     (:mySillyException e) ->
        (begin (printf "caught exception: " e "\n")
               2 ;; we have to result in the same type as the try expression
        )
  )

No MultiMethods or Argument-Type Overloading

One of the consequences of Irken's static typing, is that function arguments must have a (possibly parametric) static type. This means it's not possible to have a single function operate over different types. For example, (= a b) has the type-signature (int int -> bool).

This means, for example, it's not possible to write a single function which takes arguments of anytype. Instead, one can use macros with sublanguages to make using multiple types less inconvenient. For example, the (format ...) macro, also usable within (printf ...) enables printing of different types of immediate values.

(printf (int 1) "two" (sym `three))

Next: Reader Syntax

Clone this wiki locally