Motoko 智能合约语言的简明概述

Concise overview of Motoko

此处记录的 Motoko 版本(当前为 0.9.3)可能比 dfx 附带的 Motoko 编译器早几个版本。

This is terse, slide-like introduction to Motoko and its features.  这是对 Motoko 及其功能的简洁、幻灯片式的介绍。

Motivation and Goals /设计Motoko语言动机与目标

A simple, useful language for the Internet Computer (IC)  一种简单、有用的互联网计算机 (IC) 语言

  • Familiar syntax 熟悉的语法

  • Safe by default 默认安全

  • Incorporating actor model for canister smart contracts  纳入canister容器智能合约的actor模型

  • Seamless integration of Internet Computer features  无缝整合IC互联网计算机功能

  • Making most of present and future WebAssembly  充分利用现在和未来的 WebAssembly


Key Design Points

  • Object-oriented, functional & imperative   面向对象、函数式和命令式

  • Objects as records of functions  对象作为函数的记录

  • async/await for sequential programming of asynchronous messaging    async/await 用于异步消息传递的顺序编程

  • Structural typing with simple generics and subtyping   具有简单泛型和子类型的结构类型

  • Safe arithmetic (both unbounded and checked)  安全算术(无界和检查)

  • Non-nullable types by default   默认情况下不可为 null 的类型

  • Garbage collected (no manual memory management)  内存垃圾收集(无手动内存管理)

  • JavaScript-like syntax but statically typed & sane   类似 JavaScript 的语法,但静态类型且理智

灵感来自:Java、JavaScript、C#、Swift、Pony、ML、Haskell等编程语言

Semantics 语义​

  • call-by-value (like Java, C, JS, ML; unlike Haskell)  按值调用(如 Java、C、JS、ML;与 Haskell 不同)

  • declarations are locally mutually recursive  声明是局部相互递归的

  • parametric, bounded polymorphism   参数化、有界多态性

  • subtyping as zero-cost subsumption, not coercion  子类型化是零成本包含,而不是强制

  • no dynamic casts  没有动态转换

  • no inheritance   没有继承使用方法。


Implementation(s)  语言实现

  • implemented in OCaml (leverages wasm libary)   用 OCaml 实现(利用 wasm 库)

  • simple reference interpreter  简单参考解释器

  • less simple compiler to WebAssembly   WebAssembly 不太简单的编译器

    • multipass with typed IR in each pass.    多通道,每次通道均带有类型 IR。

    • uniform representation, unboxed arithmetic  统一表示,未装箱算术

    • copying GC, compacting GC, or generational GC (select which with compiler flag)  复制 GC、压缩 GC 或分代 GC(使用编译器标志选择)

    • GC invoked after messages (for now)  消息后调用 GC(目前)

  • polymorphism by erasure   擦除多态性



The language Expressions  语言表达式​

  • Identifiers: 标识
    x,  foo_bar,  test123,  List,  Map

  • Parentheses ( … ) for grouping    用于分组的括号 (…)

  • Braces { … } for scoping (and records)  大括号 { … } 用于确定范围(和记录)

  • ;  for sequencing

  • Type annotations (to help type inference):   类型注释(以帮助类型推断):
    (42 : Int)
    (zero cost)

Libraries 函数库

import Debug "mo:base/Debug";
import Int "mo:base/Int";

(import MyLib "src/MyLib" imports a library from the local file system.)

Specific bindings can be imported from the module using object patterns

(import MyLib "src/MyLib" 从本地文件系统导入库。)


可以使用对象模式从模块导入特定的绑定

import { push; nil } = "mo:base/List";


Libraries continued

import Debug "mo:base/Debug";
import Int "mo:base/Int";
import Trie "mo:base/Trie";

type Users = Trie.Trie<Text, Nat>; // reference types

Debug.print(Int.toText(7)); // reference functions/values


Blocks and declarations 程序块和声明

type Delta = Nat;
func print() {
  Debug.print(Int.toText(counter));
};
let d : Delta = 42;
var counter = 1;
counter := counter + d;
print();
  • Semicolon after each declaration!

  • Mutually recursive

  • Mutable variables marked explicitly

Control flow 控制流

The usual suspects…​

  • do { … }

  • if b …

  • if b … else …

  • switch e { case pat1 e1; …; case _ en }

  • while b …

  • loop …

  • loop … while b

  • for (pat in e) …

  • return, return e

  • label l e, break l e

  • do ? { … e! … }

  • async e,  await e (restricted)

  • throw,  try … catch x { … } (restricted)

Primitive types

Unbounded integers

Int

{ …​,  -2,  1,  0,  1,  2, …​ }

Inferred by default for negative literals.

Literals: 13,  0xf4,  -20,  +1,  1_000_000

Unbounded naturals

Nat

{ 0,  1,  2,  …​ }

Non-negative, trap on underflow.

Inferred by default for non-negative literals

Literals: 13, 0xf4, 1_000_000

Nat <: Int

Nat is a subtype of Int

(you can supply a Nat wherever an Int is expected)

Bounded numbers (trapping)  有界数(陷阱)

Nat8,  Nat16,  Nat32,  Nat64,  Int8,  Int16,  Int32,  Int64

Trap on over- and underflow; wrap-around and bit-manipulating operations available separately

Needs type annotations (somewhere)

Literals: 13, 0xf4, -20, 1_000_000

Floating point numbers

Float

IEEE 754 double precision (64 bit) semantics, normalized NaN

Inferred for fractional literals

Literals: 0,  -10,  2.71,  -0.3e+15,  3.141_592_653_589_793_12

Numeric operations

No surprises here

- x
a + b
a % b
a & b
a << b

a + % b, a -% b, … for wrapping, modular arithmetic (where appropriate)

Characters and Text

Char, Text

Unicode! Character = Unicode scalar value; no random access on text

  • 'x',  '\u{6a}',  '☃',

  • "boo",  "foo \u{62}ar ☃"

  • "Concat" # "enation"

Booleans

Bool

Literals:  true,  false

a or b
a and b
not b
if (b) e1 else e2

Functions

Function types

  • Simple functions:

    Int.toText : Int -> Text
    
  • multiple arguments and return values

    divRem : (Int, Int) -> (Int, Int)
    
  • can be generic/polymorphic

    Option.unwrapOr : <T>(?T, default : T) -> T
    
  • first-class (can be passed around, stored)

    map : <A, B>(f : A -> B, xs : [A]) -> [B]
    let funcs : [<T>(T) -> T] = …
    

Function Declarations & Use

func add(x : Int, y : Int) : Int = x + y;

func applyNTimes<T>(n : Int, x : T, f : T -> ()) {
  if (n <= 0) return;
  f(x);
  applyNTimes(n-1, x, f);
};

applyNTimes<Text>(3, "Hello!", func(x) { Debug.print(x) } );
  • func() { … } short for func() : () = { … }

  • Parametric functions

  • Type instantiations may sometimes be omitted

  • Anonymous functions (a.k.a. lambdas)

Composite types

Tuples

(Bool, Float, Text)

immutable, heterogeneous, fixed size

let tuple = (true or false, 0.6 * 2.0, "foo" # "bar");
tuple.1;
let (_, _, t) = tuple;
t

Options

?Text

is either a value of that type, e.g. ?"hello", or null.

func display(x : ?Text) : Text {
  switch x {
    case (null) { "No value" };
    case (?y) { "Value: " # y };
  };
};
(display(null), display(?"Test"))

Option blocks

Switching on every option value can be inconvenient …​

The option block, do ? { … }, allow you to safely access option values with a postfix null break ! expression.

Within do ? { … }, which returns an option, the expression e! immediately exits the block with null when the value of option e is null or continues with the option’s contents.

func add(x : ?Nat, y: ?Nat) : ?Nat {
  do ? { x! + y! };
};

(add(null, null), add (?1,null), add (?1,?2), add (null,?2));

Arrays (immutable)

[Text]

let days = [ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" ];

assert(days.size() == 7);

assert(days[1] == "Tue");

// days[7] will trap (fixed size)

for (d in days.vals()) { Debug.print(d) };

Arrays (mutable)

[var Nat]

let counters = [var 1, 2, 3];

assert(counters.size() == 3);

counters[1] := counters[1] + 1;

// counters[3] will trap (fixed size)

counters;

Records

{first : Text;  last : Text;  salary :  var Nat}

let employee = {first = "John"; last = "Doe"; var salary = 81_932};

Debug.print(
  employee.first # " " # employee.last # " earns " #
    Int.toText(employee.salary) # " pounds."
);

employee.salary += 79_496;

employee;

Objects

{first : Text; last : Text; get : () → Nat; add : Nat → ()}

object self {
  public let first = "John";
  public let last = "Doe";
  var salary : Nat = 81_932; // private by default
  public func get() : Nat = salary;
  public func add(bump : Nat) { salary += bump };
}

Classes

class Employee(fst : Text, lst : Text) {
  public let first = fst;
  public let last = lst;
  var salary : Nat = 0;
  public func get() : Nat = salary;
  public func add(bump : Nat) { salary += bump };
}

Classes are factories for constructing objects.
A class introduces a type and a function (for constructing instances).

Just sugar for:

type Employee = {first : Text; last : Text; get : () -> Nat; add : Nat -> ()};

func Employee(fst : Text, lst : Text) : Employee = object { … }


Variants 变体

{#sun;  #mon;  #tue;  #wed;  #thu; #fri;  #sat}

type Day = {#sun; #mon; #tue; #wed; #thu; #fri; #sat};

func toText(d : Day) : Text {
  switch d {
     case (#sun) "Sunday";
     case (#mon) "Monday";
     case (#tue) "Tuesday";
     case (#wed) "Wednesday";
     case (#thu) "Thursday";
     case (#fri) "Friday";
     case (#sat) "Saturday";
   };
};

func sort(d : Day) : { #weekDay; #weekEnd } {
  switch d {
    case (#sun or #sat) #weekEnd;  // or pattern
    case _ #weekDay;  // wildcard pattern
  };
};


Recursive Types

type List = {
  #item : {head : Text; tail : List}; // variant with payload!
  #empty                              // ^^^^ recursion!
};

func reverse(l : List) : List {
  func rev(l : List, r : List) : List {
    switch l {
      case (#empty) { r };
      case (#item { head; tail }) { // nested patterns
        rev(tail, #item {head; tail = r})
      }
    }
  };
  rev(l, #empty);
};

let l = reverse(#item {head = "A"; tail = #item {head = "B"; tail = #empty}});


Generic types

type List<T> = {
  #item : {head : T; tail : List<T>};
  #empty
};

func reverse<T>(l : List<T>) : List<T> {
  func rev(l : List<T>, r : List<T>) : List<T> {
    switch l {
      case (#empty) { r };
      case (#item { head; tail }) { // a nested pattern
        rev(tail, #item {head; tail = r})
      }
    }
  };
  rev(l, #empty);
};

let s : List<Text> =
  reverse(#item {head = "A"; tail = #item {head = "B"; tail = #empty}});

let ns : List<Nat> =
  reverse(#item {head = 0; tail = #item {head = 1; tail = #empty}})


Packages and modules

Modules

// the type of base/Int.mo
module {
  type Int = Prim.Types.Int;
  toText : Int -> Text;
  abs : Int -> Nat;
  // ...
}

modules contain named types and values (like objects),
but are restricted to static content (pure, no state, …)

Module imports

import Debug "mo:base/Debug";  // import from package
import Int "mo:base/Int";
import MyLib "lib/MyLib";      // import from local file MyLib.mo

base package provides basic features as separate modules.

More libraries popping up!

MyLib.mo must contain a module or actor class, eg:

module {
  public type List<T> = …;

  public func reverse<T>(l : List<T>) : List<T> { … };
}

Platform features

Actor types

Like object types, but marked as actor:

type Broadcast = actor {
  register : Receiver -> ();
  send : Text -> async Nat;
};

type Receiver = actor {
  recv : query Text -> async Nat
};

sharable arguments and no or async result type.

  • register is a oneway IC method (unawaitable).

  • send is an IC update method

  • recv is IC query method

IC canister with Candid interface ≈ Motoko actor

sharable ≈ serializable

Sharable:

  • all primitive types

  • records, tuples, arrays, variants, options
    with immutable sharable components

  • actor types

  • shared function type

Not sharable:

  • mutable things

  • local functions

  • objects (with methods)


A complete actor

import Array "mo:base/Array";

actor {
  type Receiver = actor {recv : query Text -> async Nat};

  var r : [Receiver] = [];

  public func register(a : Receiver) {
    r := Array.append(r, [a]);
  };

  public func send(t : Text) : async Nat {
    var sum = 0;
    for (a in r.vals()) {
      sum += await a.recv(t);
    };
    return sum;
  };
}

a typical actor/canister main file

Async/await

async T

asychronous future or promise

introduced by async { … }
(implicit in async function declaration)

await e
suspends computation pending e’s result:
if the result is a value, continues with that value,
if the result is an Error, throws the error.

  public func send(t : Text) : async Nat {
    var sum = 0;
    for (a in r.vals()) {
      sum += await a.recv(t); // may return Nat or `throw` error
    };
    return sum;
  };

(Errors can be handled using try … catch …)

Concurrency Hazards

Functions that await are not atomic.
Suspension introduces concurrency hazards.

A bad implementation of send:

  var sum = 0; // shared state!
  public func send(t : Text) : async Nat {
    sum := 0;
    for (a in r.vals()) {
      sum += await a.recv(t);
    };
    return sum;
  };

(Concurrent sends will share and clobber sum.)

Beware of race conditions!


Actor import

import Broadcast "canister:Broadcast";
/* or
import Broadcast "ic:r7inp-6aaaa-aaaaa-aaabq-cai";
*/
actor Self {

  var count = 0;

  public func go() {
    Broadcast.register(Self);
  };

  public query func recv(msg : Text) : async Nat {
    return count;
  }
}

(assumes there is a Candid file describing the interface of the import)

A Candid interface file

Broadcast's Candid file (produced by moc --idl Broadcast.mo compiler).

Broadcast.did:

type Receiver =
 service {
   recv: (text) -> (nat) query;
 };
service : {
  register: (Receiver) -> () oneway;
  send: (text) -> (nat);
}

A language independent interface definition.

Could just as easily describe a Rust implementation of Broadcast.


Principal and caller

import Principal "mo:base/Principal";

actor Self {

  public shared(context) func hello() : async Text {
    let myself : Principal = Principal.fromActor(Self);
    if (context.caller == myself) {
      "Talking to yourself is the first sign of madness";
    } else {
      "Hello, nice to see you";
    };
  };

}

Errors

import Principal "mo:base/Principal";
import Error "mo:base/Error";

actor Self {

  public shared(context) func hello() : async Text {
    let myself : Principal = Principal.fromActor(Self);
    if (context.caller == myself) {
      throw Error.reject("Talking to yourself is the first sign of madness");
    } else {
      "Hello, nice to see you";
    };
  };

};

async {
  let t = try Self.hello() catch (e) { Error.message(e); }
};

Similar to exceptions in other languages,
but only available in async contexts, e.g. shared functions; async blocks

Stable variables/Stable 变量

If we upgrade the Broadcast actor, all current registrations are lost.
To preserve them, declare the state variable r as stable.

import Array "mo:base/Array";

actor Broadcast {

  type Receiver = actor {recv : query Text -> async Nat};

  stable var r : [Receiver] = []; // declare r `stable`

  public func register(a : Receiver) { … }
  public func send(t : Text) : async Nat { … }

  // optional pre-upgrade action
  system func preupgrade() { Debug.print("saving receivers"); }

  // optional post-upgrade action
  system func postupgrade() {  Debug.print("restoring receivers"); }
}

stable variables must have stable types (see manual)
system hooks can’t send messages

Type system

Structural

/*
type List = {
  #item : {head : Text; tail : List};
  #empty
};

func reverse(l : List) : List { //... };
*/
type Stack = {
   #empty;
   #item : {tail : Stack; head : Text};
};

let stack : Stack = #empty;

let revStack = reverse(stack); // works though reverse defined on List (not Stack)

Type definitions
do not create types,
but name existing types

Despite their different names, Stack and List are equivalent types.

Subtyping (Variants)

WeekDay <: Day

type WeekDay = {#mon; #tue; #wed; #thu; #fri};

type Day = {#sun; #mon; #tue; #wed; #thu; #fri; #sat};

func toText(d : Day) : Text {
  switch d
   { case (#sun) "Sunday";
     case (#mon) "Monday";
     //...
   };
};

let mon : WeekDay = #mon;
let t = toText(mon); // also works, since WeekDay <: Day

t1 <: t2: t1 can be used wherever t2 is expected

Employee <: Person


type Employee = {first : Text; last : Text; var salary : Nat};
type Person = {first : Text; last : Text};

func toText(p : Person) : Text {
  p.last # "," # p.first;
};

let employee : Employee =
  { first = "John"; last = "Doe"; var salary = 161_401};

let t = toText(employee); // also works, since Employee <: Person

Fin

Not covered

  • Polymorphic functions with type bounds

  • User defined iterator objects, supporting for loops.

  • Actor classes

  • debug_show for conversion of almost any value to text.

  • debug e expressions for debug-only compilation

  • do ? { … e! … } blocks for handling/propagating option values.

  • assert e expressions for conditional traps

  • tools:

    • mo_doc (generates doc from doc comments),

    • vessel (package manager)

    • mo_ide (LSP language server for VSCode, emacs etc)


Motoko 智能合约语言的简明概述
arkMeta Crypto Network Limited, arkSong 2023年10月31日
标签
登录 留下评论

Motoko 编程语言指南
Motoko Programming Language Guide