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
.