Skip to content

Values and types ​

Pure. Bindings and annotations, type aliases, records against maps, unions and optional fields, case type dispatch, cast, and a generic function.

bash
lask eval config-summary
lask eval describe 42 ; lask eval describe true ; lask eval describe hi

The thing to take away: a?: T and a: T | Null answer different questions, and neither is ever inferred — each one is something you wrote.

To try the commands above, paste the source below into main.lask in an empty directory, or clone the repository:

bash
git clone https://github.com/lask-task-runner/lask.git
cd lask/example/02-language/01-values-and-types

Source ​

This is example/02-language/01-values-and-types in the lask repository.

lask
// Values and types
//
// Everything in this file is pure, so it runs with `lask eval` alone —
// no image, no Docker.
//
//   lask check
//   lask eval config_summary
//   lask eval describe 42

// A top-level binding is a value, not a task: it is evaluated lazily,
// once, the first time something needs it.
app = "orders"
port: Number = 8080

// A type alias names a shape once so the rest of the file can refer to it.
type Port = Number

// `a: T | Null` is about the value, `a?: T` is about the key. They are
// different questions, and neither is ever inferred: a union and an
// optional field only ever exist because someone wrote one.
type Config = Record<
  name: String,
  port: Port,
  replicas: Number | Null,
  tags?: Array<String>
>

// The optional key may simply be left out of the literal.
staging: Config = {
  name: app,
  port: port,
  replicas: null
}

production: Config = {
  name: app,
  port: 443,
  replicas: 3,
  tags: ["prod", "eu-west-1"]
}

// Summarize every configuration in this module.
//
// A record has a statically fixed field set, so `.field` can never fail
// at runtime, and an absent optional key simply reads as null.
//
// @return One line per environment.
config_summary(): Array<String> = [
  describe_config(staging),
  describe_config(production)
]

// Render one configuration as a line of text.
describe_config(c: Config): String = do {
  // `c.replicas` is `Number | Null`: interpolating it would be a type
  // error, so the union is resolved first. `case` is the only thing
  // that narrows, and only on a plain local name.
  n = c.replicas
  scale = case (n) {
    Null -> "unscaled"
    else -> "x#{n}"
  }
  "#{c.name}:#{c.port} #{scale} [#{join(tags_of(c), ",")}]"
}

// Read the optional `tags` key as an array.
//
// An optional key reads as `Array<String> | Null`, resolved the same way.
tags_of(c: Config): Array<String> = do {
  t = c.tags
  case (t) {
    Null -> []
    else -> t
  }
}

// A map is for keys that vary at runtime; a record is for a field set
// known at compile time. `m[k]` fails on a missing key, `get_or` does not.
limits: Map<Number> = { "cpu": 2, "memory": 4096 }

// Look a limit up by name, with a fallback.
limit_of(name: String): Number = get_or(limits, name, 0)

// Say what kind of value this is.
//
// Type dispatch is how a value gets out of `Any` or a union. Each arm
// narrows the scrutinee, and the `else` arm is mandatory and last.
//
// @param v  Anything at all — the CLI decodes `42` as a Number and
//           `hi` as a String.
// @example lask eval describe true
describe(v: Any): String = case (v) {
  Null   -> "nothing"
  Number -> "number #{v}"
  Bool   -> "bool #{v}"
  String -> "string #{v}"
  else   -> "something structured: #{to_json(v)}"
}

// Force an `Any` to be a Number.
//
// `cast` is the other way out of `Any`: it fails on anything else,
// where `case` tests instead.
as_number(v: Any): Number = cast(v)

// The first element, or a fallback when there is none.
//
// A type parameter is opaque inside its own declaration — it conforms
// only to itself and `Any` — so the body may move a `T` around but not
// compare or interpolate one. It is never written at a call site.
first_or<T>(xs: Array<T>, fallback: T): T =
  is_empty(xs) ? fallback : xs[0]

// Instantiate `first_or` at Number.
//
// Instantiated at Number here, at String below.
head_number(): Number = first_or([], 0)

// Instantiate `first_or` at String.
head_name(): String = first_or(["lask"], "none")

Released under the MIT License.