Ship

Book · Inference

Inference

Draft

Hindley–Milner infers ordinary code, so most of it never writes a type.

On this page · 7 sections

Literals

Every literal has exactly one type.

Literals

test✓
  ✓11: Int: Int;
  ✓TrueTrue: Bool: Bool;
  ✓"ship""ship": String: String;
  ✓()(): Unit: Unit;
  ✓(11: Int, "ship""ship": String): (Int, String);
  ✓(TrueTrue: Bool, FalseFalse: Bool): (Bool, Bool);

An unconstrained [] stays polymorphic. A filled vector does not.

Empty Collections

test✓
  ✓[]: Vector('a);
  ✓[11: Int, 22: Int, 33: Int]: Vector(Int);
  ✓listlist: pure fn!('a)(...Int) -> List(Int)(11: Int, 22: Int, 33: Int): List(Int);

Functions

Write the body. The function type follows. See functions.

let doubledouble: fn(n: Int) -> Int = (nn: Int) => nn: Int + nn: Int;

Function Bodies

test✓
  ✓doubledouble: fn(n: Int) -> Int: fn(Int) -> Int;
  expect ✓doubledouble: fn(n: Int) -> Int(2121: Int) == 4242: Int;

One explicit parameter can still infer the rest.

let addadd: fn(left: Int, right: Int) -> Int = fn(leftleft: Int: Int, rightright: Int) { leftleft: Int + rightright: Int };

Mixed Annotations

test✓
  ✓addadd: fn(left: Int, right: Int) -> Int: fn(Int, Int) -> Int;
  expect ✓addadd: fn(left: Int, right: Int) -> Int(55: Int, 77: Int) == 1212: Int;

A value that only flows through stays polymorphic.

let idid: fn(value: 'a) -> 'a = fn(valuevalue: 'a) { valuevalue: 'a };

Identity

test✓
  ✓idid: fn(value: 'a) -> 'a: fn(value: 'a) -> 'a;
  expect ✓idid: fn(value: Int) -> Int(11: Int) == 11: Int;
  expect ✓idid: fn(value: String) -> String("ship""ship": String) == "ship""ship": String;

Explicit binders name the type variables. The names are local, so 'left and 'right are the same type here.

let identityidentity: fn('left)(value: 'left) -> 'left = fn('left)(valuevalue: 'a: 'left) -> 'left { valuevalue: 'a };

Explicit Binders

test✓
  ✓identityidentity: fn('left)(value: 'left) -> 'left: pure fn!('right)(value: 'right) -> 'right;

Products

Declare the data once. Functions do not restate it.

type User = {
  name: String;
  age: Int;
}

// Infers `user: User` because of `.name` field access.
let namename: fn(user: Book.Inference.User) -> String = fn(useruser: Book.Inference.User) { useruser: Book.Inference.User.namename: String };

let pair_with_countpair_with_count: fn(user: Book.Inference.User, count: 'a) -> (Book.Inference.User, 'a) = fn(useruser: Book.Inference.User: User, countcount: 'a) { (useruser: Book.Inference.User, countcount: 'a) };

Products

test✓
  let adaada: Book.Inference.User = { name = "Ada""Ada": String; age = 3636: Int; };
  adaada: Book.Inference.User;
  User { name = "Ada"; age = 36 }
  ✓namename: fn(user: Book.Inference.User) -> String: fn(User) -> String;
  expect ✓namename: fn(user: Book.Inference.User) -> String(adaada: Book.Inference.User) == "Ada""Ada": String;

  ✓pair_with_countpair_with_count: fn(user: Book.Inference.User, count: 'a) -> (Book.Inference.User, 'a): fn(user: User, count: 'a) -> (User, 'a);
  expect ✓pair_with_countpair_with_count: fn(user: Book.Inference.User, count: Int) -> (Book.Inference.User, Int)(adaada: Book.Inference.User, 11: Int) == (adaada: Book.Inference.User, 11: Int);

Type aliases compare as the same structure.

type Count = Int

Aliases

test✓
  let countcount: Int = 55: Int;
  ✓countcount: Int: Int;
  ✓countcount: Int: Count;

Pipes

|> fills the first positional. The result is an ordinary inferred value.

let describedescribe: fn(value: Int, ~label: String) -> String = fn(valuevalue: Int, ~labellabel: String) { labellabel: String ++ ":"":": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(valuevalue: Int) };

Pipes

test✓
  let doubleddoubled: Int = 55: Int |> doubledouble: fn(n: Int) -> Int;
  expect ✓doubleddoubled: Int == 1010: Int;

  let describeddescribed: String = 55: Int |> describedescribe: fn(value: Int, ~label: String) -> String(label = "value""value": String);
  expect ✓describeddescribed: String == "value:5""value:5": String;

  let chainedchained: String = 22: Int
    |> doubledouble: fn(n: Int) -> Int
    |> doubledouble: fn(n: Int) -> Int
    |> describedescribe: fn(value: Int, ~label: String) -> String(label = "chained""chained": String);
  expect ✓chainedchained: String == "chained:8""chained:8": String;

  let groupedgrouped: Int = 77: Int |> (doubledouble: fn(n: Int) -> Int);
  expect ✓groupedgrouped: Int == 1414: Int;

  let bare_pathbare_path: String = 4242: Int |> Int.to_stringto_string: pure fn!(value: Int) -> String;
  expect ✓bare_pathbare_path: String == "42""42": String;

Partial application

f(args, ...) infers a function of the remaining parameters, including labels and the authored default expressions.

let taggedtagged: fn(value: Int, ~label: String, ~suffix: String = "") -> String = fn(valuevalue: Int, ~labellabel: String, ~suffixsuffix: String = """": String) {
  labellabel: String ++ ":"":": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(valuevalue: Int) ++ suffixsuffix: String
};

Partial Application

test✓
  ✓addadd: fn(left: Int, right: Int) -> Int: fn(Int, Int) -> Int;
  let add5add5: fn(right: Int) -> Int = addadd: fn(left: Int, right: Int) -> Int(55: Int, ...);

  ✓add5add5: fn(right: Int) -> Int: fn(Int) -> Int;
  expect ✓add5add5: fn(right: Int) -> Int(77: Int) == 1212: Int;

  let labelledlabelled: fn(value: Int, ~suffix: String = "") -> String = taggedtagged: fn(value: Int, ~label: String, ~suffix: String = "") -> String(label = "n""n": String, ...);
  ✓labelledlabelled: fn(value: Int, ~suffix: String = "") -> String: fn(Int, ~suffix: String = "") -> String;
  expect ✓labelledlabelled: fn(value: Int, ~suffix: String = "") -> String(33: Int) == "n:3""n:3": String;
  expect ✓labelledlabelled: fn(value: Int, ~suffix: String = "") -> String(33: Int, suffix = "!""!": String) == "n:3!""n:3!": String;

  let described_onedescribed_one: fn(~label: String, ~suffix: String = "") -> String = taggedtagged: fn(value: Int, ~label: String, ~suffix: String = "") -> String(11: Int, ...);
  ✓described_onedescribed_one: fn(~label: String, ~suffix: String = "") -> String: fn(~label: String, ~suffix: String = "") -> String;
  expect ✓described_onedescribed_one: fn(~label: String, ~suffix: String = "") -> String(label = "one""one": String) == "one:1""one:1": String;

Channels

Inferred function types carry exn when the body can fail.

let require_nonnegativerequire_nonnegative: fn(value: Int) -> Int, exn [ :negative ] = fn(valuevalue: Int) {
  if valuevalue: Int < 00: Int {
    error :negative;
  }
  valuevalue: Int
};

let require_positiverequire_positive: fn(value: Int) -> Int, exn [ :nonpositive(Int) ] = fn(valuevalue: Int) {
  if valuevalue: Int <= 00: Int {
    error :nonpositive(valuevalue: Int);
  }
  valuevalue: Int
};

let sum_checkedsum_checked: fn(value: Int) -> Int, exn [ :negative | :nonpositive(Int) ] = fn(valuevalue: Int) {
  let xx: Int = require_nonnegativerequire_nonnegative: fn(value: Int) -> Int, exn [ :negative ](valuevalue: Int);
  let yy: Int = require_positiverequire_positive: fn(value: Int) -> Int, exn [ :nonpositive(Int) ](valuevalue: Int);
  xx: Int + yy: Int
};

let sum_mustsum_must: fn(value: Int) -> Int, exn [ :negative ] = fn(valuevalue: Int) {
  let xx: Int = require_nonnegativerequire_nonnegative: fn(value: Int) -> Int, exn [ :negative ](valuevalue: Int);
  let yy: Int = must require_positiverequire_positive: fn(value: Int) -> Int, exn [ :nonpositive(Int) ](valuevalue: Int);
  xx: Int + yy: Int
};

Exceptions

test✓
  ✓require_nonnegativerequire_nonnegative: fn(value: Int) -> Int, exn [ :negative ]: fn(value: Int) -> Int, exn [ :negative ];
  expect ✓must require_nonnegativerequire_nonnegative: fn(value: Int) -> Int, exn [ :negative ](33: Int) == 33: Int;

  ✓require_positiverequire_positive: fn(value: Int) -> Int, exn [ :nonpositive(Int) ]: fn(value: Int) -> Int, exn [ :nonpositive(Int) ];
  expect ✓must require_positiverequire_positive: fn(value: Int) -> Int, exn [ :nonpositive(Int) ](33: Int) == 33: Int;

  ✓sum_checkedsum_checked: fn(value: Int) -> Int, exn [ :negative | :nonpositive(Int) ]: fn(value: Int) -> Int, exn [ :negative | :nonpositive(Int) ];
  expect ✓must sum_checkedsum_checked: fn(value: Int) -> Int, exn [ :negative | :nonpositive(Int) ](33: Int) == 66: Int;

  ✓sum_mustsum_must: fn(value: Int) -> Int, exn [ :negative ]: fn(value: Int) -> Int, exn [ :negative ];
  expect ✓must sum_mustsum_must: fn(value: Int) -> Int, exn [ :negative ](33: Int) == 66: Int;

Units of measure

Dimensionless Int and measured Int<m> are different types.

unit m;

Units Of Measure

test✓
  ✓1<m>: Int<m>;
  ✓1<m / m>: Int;