Surprises

SCaml is designed to minimize the surprises of its programmers: Some of OCaml language features are not available, but they are clearly listed.

Unfortunatelly, there are still some amount of surprises. Some class of valid OCaml programs are rejected by SCaml compiler even though they only use supported features.

Many constuctors only take constants

Many constructors defined in SCaml are used to write smart contract specific constants. They only take constants as arguments.

For example Int 3 and Nat 42 are valid, but the following expressions are invalid:

(* Error: [ESCaml200] Int can only take an integer constant *)
let x = 20 in Int x

(* Error: [ESCaml200] Int can only take an integer constant *)
Nat (int_of_float 10.0)

This applies to blockchain related constant constructors such as Address "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN". You cannot extract the string of the address by pattern matching like:

(* Error: [ESCaml200] Address only takes a string literal *)
fun (Address s) -> s

You cannot create an address from a string either like:

(* Error: [ESCaml200] Address only takes a string literal *)
let s = "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" in Address s

No partial application of primitives

Functions defined in SCaml are all primitives. They must be fully applied:

(* Error: [ESCaml000] SCaml does not support partial application of primitive (here SCaml.+) *)
let incr = (+) 1

Unintuitive type constraints are required sometimes

SCaml is monomorphic. If a type of its expression contains type variables, SCaml rejects it. Explicit type constraints are required to instantiate these type variables.

Users may find type constraints are required at unexpected places:

(* Error: [ESCaml100] This expression has type (SCaml.int, 'a) SCaml.sum, whose type variable 'a is not supported in SCaml. *)
match Left (Int 1) with
| Left x -> x
| Right y -> y

In this case you have to constrain the type of Left (int 1) to (int, int) sum:

(* valid *)
match (Left (Int 1) : (int, int) sum) with
| Left x -> x
| Right y -> y

For whom familiar with OCaml, this is a bit puzzling, since at a glance the expression looks to have type (int, int) sum without the constraint because the argument type of Right is unified with the one of Left in the pattern match cases, but it does not actually. OCaml's type system is not so simple as the ordinary ML typing, and it performs more powerful type inferenece. Here, it generalizes the type of the expression just as if we had a let binding for the match target:

(* Invalid *)
match let v = Left (Int 1) in v with
| Left x -> x
| Right y -> y

Due to this virtual let binding, the expression has a generalized type (int, 'a) sum which is rejected by SCaml.

Function bodies cannot have non packable free variables in the bodies

The most puzzling surprise of SCaml.

SCaml's functions do not allow free variable occurrences of non packable types in their bodies.

Non packable types are SCaml.big_map, SCaml.operation, SCaml.contract, and types containing them.

The free variable occurrences of a function are variables defined outside of the function body. For example, fun x -> (x, y, let z = 1 in z), y is the sole free variable occurrence of the function; x is bound by the function abstractrion and z is locally defined.

A function is rejected in SCaml if it is not an entry point and has a free variable occurrence of a non packable type.

It sounds strange, but it is a restriction derived from the same limitation of Michelson's functional values. To explain further for who are familiar with functional programming, Michelson closures must be packable and therefore they cannot have non packable types in their environments.

Note that this restriction is not applied to the entry point functions; they are handled without Michelson closures.

Example

This is an invalid example of a voting system:

(* Error: [ESCaml400] Function body cannot have a free variable occurrence `bigmap` with unpackable type. *)
open SCaml

let vote (k : string) bigmap =
  let incr k = 
    match BigMap.get k bigmap with
    | None -> BigMap.update k (Some (Nat 1)) bigmap
    | Some n -> BigMap.update k (Some (n +^ Nat 1)) bigmap
  in
  [], incr k

The contract takes a candidate name k to vote and increment the number of votes for k in the big map bigmap. If there is no binding for k, it initializes with Nat 1.

This definition is rejected by SCaml because the variable bigmap of a non packable type (string, nat) big_map is ocurring freely in the function body of incr.

Fix 1: inline the function

You can avoid this problem by removing the function incr by hand inlining:

open SCaml

let vote (k : string) bigmap =
  let bigmap' = match BigMap.get k bigmap with
    | None -> BigMap.update k (Some (Nat 1)) bigmap
    | Some n -> BigMap.update k (Some (n +^ Nat 1)) bigmap
  in
  [], bigmap'

Without the function, the problem is also gone.

Fix 2: parameterize the variable

You can also work around by abstracting the variable:

open SCaml

let vote (k : string) bigmap =
  let incr (k : string) bigmap = 
    match BigMap.get k bigmap with
    | None -> BigMap.update k (Some (Nat 1)) bigmap
    | Some n -> BigMap.update k (Some (n +^ Nat 1)) bigmap
  in
  [], incr k bigmap

In this code, the occurrences of bigmap in the function is bound by the function, therefore they are no longer free.

You have to be careful of the ordering of the abstractions. The following code with a different order of arguments is rejected:

(* Error: [ESCaml400] Function body cannot have a free variable occurrence `bigmap` with unpackable type. *)
open SCaml

let vote (k : string) bigmap =
  let incr bigmap (k : string) = 
    match BigMap.get k bigmap with
    | None -> BigMap.update k (Some (Nat 1)) bigmap
    | Some n -> BigMap.update k (Some (n +^ Nat 1)) bigmap
  in
  [], incr bigmap k

This is because bigmap occurs freely inside the body of the function abstraction of (k : string).

Fix 3: uncurrying

If a function has a type t1 -> t2 -> t3 and t1 and t2 are not packable either, for example operation list -> contract -> operation list you cannot work around the restriction by reordering them. In this case, you can uncurry the arguments:

(* Error: [ESCaml400] Function body cannot have a free variable occurrence `bigmap` with unpackable type. *)
let add_transfer ops c = 
	Operation.transfer_tokens () (Tz 1.0) c :: ops 
in 
...
	

The above function takes 2 arguments and they are not packable. As far as the function is in curried form, you cannot compile it in SCaml.

In this case you can turn the function into uncurried form, which takes 1 argument of a pair of ops and c:

(* valid *)
let add_transfer (ops, c) = 
	Operation.transfer_tokens () (Tz 1.0) c :: ops 
in 
...
	

Now the code has only 1 function abstraction, and ops and c are both bound by it.

Fix 4: Use uncurried primitives

This restriction is troublesome especially when you use higher order function primitives like List.fold_left with non packable types. Here is an invalid example which tries to send tokens to each member of a contract list:

(* Error: [ESCaml400] Function body cannot have a free variable occurrence `bigmap` with unpackable type. *)
List.fold_left
  (fun ops -> fun c -> Operation.transfer_tokens () (Tz 1.0) c :: ops)
  [] contracts

Though this coding style with List.fold_left is quite common in OCaml, this expression is rejected by SCaml, since the type ops is SCaml.operation list which is not packable and ops is freely occurring in the inner function fun c -> ...

You cannot fix the issue by defining uncurried version of List.fold_left, by yourself, since it requires recursion and polymorphism. To work around this specific problem, SCaml provides another version of list folding List.fold_left':

List.fold_left'
  (fun (ops, c) -> Operation.transfer_tokens () (Tz 1.0) c :: ops)
  [] contracts

Here, the two function abstractions in the previous example are “uncurried” i.e. squashed into one which takes a tuple (ops, c), thus making the occurrence of ops is no longer free. SCaml provides this kind of uncurried foldings for list, set, and map.


Last modified September 11, 2020: 1.2.0 update (5b9651c)