Skip to content

Control flow ​

Pure. do blocks, if as an expression with or without else, c ? a : b, all three forms of case, and for.

bash
lask eval plan-release --dry-run true
lask eval classify 503
lask eval checklist ; lask eval steps

The thing to take away: every case needs an else, last, always — exhaustiveness is never inferred — and case is the only thing that narrows a union.

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/03-control-flow

Source ​

This is example/02-language/03-control-flow in the lask repository.

lask
// Control flow
//
// `do`, `if`, `case` and `for` — all of them expressions, all pure here.
//
//   lask eval classify 404
//   lask eval plan_release --dry-run true
//   lask eval checklist

// Release, or report what a release would do.
//
// A `do` block is a sequence whose value is that of its last statement.
// Bindings are local to it and evaluated in the order they are written.
//
// @param dry_run  Report what would happen instead of doing it.
// @example lask eval plan-release --dry-run true
plan_release(--dry_run: Bool = false, --version: String = "1.0.0"): String = do {
  target = "app:#{version}"
  // There is no `return`: the value of a block is its last statement,
  // so the other way out is the `else` block.
  if (dry_run) { "would release #{target}" } else {
    note = stamp(target)
    "released #{note}"
  }
}

internal stamp(target: String): String = "#{target} (#{length(target)} chars)"

// Say whether a number is even or odd.
//
// With `else`, both branches produce a value, of the same type, and
// that is the type of the `if`.
parity(n: Number): String = if (mod(n, 2) == 0) { "even" } else { "odd" }

// Name the tag a release would push, if it pushes one.
//
// Without `else`, an `if` is `null` when its condition is false, so its
// type is that of its block or `Null`. A block run for its effect, of
// type `Void`, leaves the `if` `Void`, as an empty `else` would.
//
// @example lask eval release-tag --publish true
release_tag(--publish: Bool = false, --version: String = "1.0.0"): String | Null = do {
  if (publish) { log("tagging v#{version}") }
  if (publish) { "v#{version}" }
}

// Say whether a number is positive, zero or negative.
//
// `c ? a : b` is `if (c) { a } else { b }` for branches that are one
// expression each. It binds more loosely than every other operator and
// nests to the right, so two conditions read top to bottom; for more,
// prefer the `case` with no scrutinee below.
//
// @example lask eval sign -- -3
sign(n: Number): String = n > 0 ? "positive" : n == 0 ? "zero" : "negative"

// Map a shell name to its family.
//
// `case` over a value: the scrutinee is evaluated once, arms may list
// several literals, and exactly one `else`, last, always. Exhaustiveness
// is never inferred.
//
// @example lask eval family zsh
family(shell: String): String = case (shell) {
  "bash", "zsh", "sh" -> "posix"
  "fish"              -> "fish"
  else                -> fail(error(4, "unknown shell: #{shell}"))
}

// Classify an HTTP status code.
//
// `case` with no scrutinee dispatches on conditions, in place of the
// chain of `else if` the language does not have.
//
// @example lask eval classify 503
classify(status: Number): String = case {
  status >= 500 -> "server error"
  status >= 400 -> "client error"
  status >= 300 -> "redirect"
  else          -> "ok"
}

// Render a value that may be absent.
//
// `case` over a type narrows the scrutinee inside each arm — and it is
// the only thing that narrows: `if (x == null)` narrows nothing. The
// scrutinee has to be written as a plain local name for that to happen.
render(v: Number | String | Null): String = case (v) {
  Null   -> "(none)"
  Number -> "#{v} items"      // v: Number here
  else   -> v                 // v: String here, the only member left
}

// Classify a handful of codes at once.
//
// `for` is an expression too: it evaluates to an `Array<R>`.
// It iterates arrays only.
//
// @return One label per status code.
checklist(): Array<String> = for (s : [200, 302, 404, 500]) {
  "#{s} -> #{classify(s)}"
}

// Number a list of strings.
//
// Counting is `range`, and an index comes from `enumerate` — `for`
// itself has no numeric or indexed form.
numbered(xs: Array<String>): Array<String> =
  for (e : enumerate(xs)) { "#{e.index + 1}. #{e.value}" }

// Number the build steps.
steps(): Array<String> = numbered(["build", "test", "deploy"])

// A map is traversed through `entries`, for the same reason.
settings: Map<String> = { "region": "eu-west-1", "stage": "prod" }

// Write every setting as `key=value`.
dump(): Array<String> = for (e : entries(settings)) { "#{e.key}=#{e.value}" }

// Classify a code and say what to do about it.
//
// An arm body is a single expression, so several statements go in a
// `do` block. Note that `{` directly after `->` starts an object
// literal, not a block.
report(status: Number): Record<level: String, text: String> = case {
  status >= 500 -> do {
    level = "error"
    { level: level, text: "#{status}: page someone" }
  }
  else -> { level: "info", text: "#{status}: nothing to do" }
}

Released under the MIT License.