Skip to content

Commit

Permalink
Add documentation for extended patterns
Browse files Browse the repository at this point in the history
This commit update the manual to take into account the latest extensions
of pattern matching, namely wildcard patterns, constant patterns, array
patterns, pattern guards and or-patterns.

Doing so, we also update the examples (in the manual and in the
`examples` directory) to use pattern matching whenever it looks more
idiomatic and make the code more readable.
  • Loading branch information
yannham committed May 27, 2024
1 parent 3b44952 commit e4abda6
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 78 deletions.
14 changes: 7 additions & 7 deletions doc/manual/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ custom contract:
```nickel
{
IsFoo = fun label value =>
if std.is_string value then
if value == "foo" then
value
else
std.contract.blame_with_message "not equal to \"foo\"" label
else
std.contract.blame_with_message "not a string" label,
value |> match {
"foo" => value,
value if std.is_string value =>
std.contract.blame_with_message "not equal to \"foo\"" label,
_ =>
std.contract.blame_with_message "not a string" label,
}
}
```

Expand Down
8 changes: 6 additions & 2 deletions doc/manual/correctness.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ using contract and type annotations.
`split` can be given a contract annotation as follows:

```nickel #no-check
split | forall a. Array {key: String, value: a} -> {keys: Array String, values: Array a} = # etc.
split
| forall a. Array {key: String, value: a}
-> {keys: Array String, values: Array a} = # etc.
```

Contract annotations are checked at runtime. At this point functions are
Expand Down Expand Up @@ -266,7 +268,9 @@ that:
`split` can be given a type annotation as follows:

```nickel #no-check
split : forall a. Array {key: String, value: a} -> {keys: Array String, values: Array a} = # etc.
split
: forall a. Array {key: String, value: a}
-> {keys: Array String, values: Array a} = # etc.
```

Type annotations also give rise to contracts, which means that even if `split`'s
Expand Down
2 changes: 2 additions & 0 deletions doc/manual/merging.md
Original file line number Diff line number Diff line change
Expand Up @@ -607,10 +607,12 @@ argument), we do get a contract violation error:
required_field2,
}
in
let intermediate =
{ foo | FooContract }
& { foo.required_field1 = "here" }
in
intermediate
& { foo.required_field2 = "here" }
|> std.deep_seq intermediate
Expand Down
68 changes: 40 additions & 28 deletions doc/manual/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,10 +666,10 @@ same language of patterns, described in the following section.
A pattern starts with an optional alias of the form `<ident> @ <inner
pattern>`. The inner pattern is either:

- an `any` pattern, which is just an identifier, and will match any value.
`any` patterns bring a new variable into scope and can be nested inside a
larger pattern. Said variables are bound to the corresponding constituent
parts of the matched value.
- an `any` pattern, which is just an identifier that will match any value.
`any` patterns bring a new variable into scope and can be nested inside a
larger pattern. Said variables are bound to the corresponding constituent
parts of the matched value.
- a wildcard pattern `_` which acts like an `any` pattern (matches
anything) but doesn't bind any variable.
- a constant pattern, which is a literal value: a number, a boolean, a string,
Expand All @@ -679,10 +679,10 @@ pattern>`. The inner pattern is either:
- an array pattern
- an or-pattern

Additionally, pattern can be guarded by an `if` condition. The last forms, as
well as pattern guards, are detailed in the following subsections. You can find
complete examples of patterns in the section on match expressions and
destructuring.
Additionally, pattern can be guarded by an `if` condition. Enum patterns, record
patterns, array patterns, or-patterns and pattern guards are detailed in the
following subsections. Complete examples of patterns are given in the section on
match expressions and destructuring.

#### Enum pattern

Expand All @@ -706,11 +706,11 @@ literals. A record pattern is a list of field patterns enclosed into braces of
the form `{ <field_pat1>, .., <field_patn>, <rest?> }`.

A field pattern is of the form `<ident> <annot?> = <pat>`, where `<pat>` is a
sub-pattern matching the content of the field. For example, `foo=bar` and
`foo='Ok value` are valid field patterns. The `= <pat>` part can be omitted when
sub-pattern matching the content of the field. For example, `foo = bar` and
`foo = 'Ok value` are valid field patterns. The `= <pat>` part can be omitted when
`<pat>` is an `any` pattern with the same name as the field: that is,
`some_field` is a valid field pattern and is just shorthand for
`some_field=some_field`.
`some_field = some_field`.

The optional annotation `<annot>` can include either:

Expand All @@ -722,22 +722,24 @@ The optional annotation `<annot>` can include either:
A contract annotation and a default annotation can be combined.

**The presence or the absence of a contract annotation never changes whether or
not a pattern matches a value**. For example, both `{foo}`, `{foo | Number}` and
`{foo | String}` match the value `{foo = "hello"}`. The difference is that `{foo
| Number}` will result in a later contract error if `foo` is ever used. The
contract annotation is merely a convenient way to apply a contract to a value
extracted from the pattern match on the fly.
not a pattern matches a value**. For example, all of `{foo}`, `{foo | Number}`
and `{foo | String}` match the value `{foo = "hello"}`. The difference is that
`{foo | Number}` will result in a later contract error if `foo` is ever used.
The contract annotation is merely a convenient way to apply a contract to a
value extracted from the pattern match on the fly.

On the other hand, a default annotation does make a difference on matching:
`{foo ? 5}` matches `{}` (and will bind `foo` to the default value `5`), but
the pattern `{foo}` doesn't match `{}`.
`{foo ? 5}` matches `{}` (and will bind `foo` to the default value `5`), but the
pattern `{foo}` doesn't match `{}`. Note that default values don't propagate to
aliases: `whole @ {foo ? 5}` will match `{}` and assigns `whole` to `{}` and
`foo` to `5`. Note that `whole` is *not* `{foo = 5}`.

The optional `<rest?>` part is either an ellipsis `..` or a capture `..<ident>`.
By default, record patterns are closed, meaning that they won't match a record
with additional fields: `{foo, bar}` doesn't match `{foo = 1, bar = 2, baz =
3}`.

The ellipsis `..` makes the pattern open, which will match a record with
The ellipsis `..` makes the pattern open. An open pattern matches a record with
additional fields. A capture has the same effect but also captures the rest of
the matched record in a variable. For example, matching `{foo, ..rest}` with
`{foo = 1, bar = 2, baz = 3}` will bind `foo` to `1` and `rest` to the record
Expand Down Expand Up @@ -774,17 +776,18 @@ Additionally, enum variant patterns must be parenthesized at the top-level of an
or-pattern branch for readability reasons. For example, `'Foo x or 'Bar x` isn't
a valid or-pattern, but `('Foo x) or ('Bar x)` is. Similarly, `'Par or or 'Plus
or` isn't a valid or-pattern, but `('Par or) or ('Plus or)` is (in this case,
the `or` inside the parentheses are just normal pattern variables).
the `or` inside the parentheses is just a normal pattern variable).

Or-patterns can optionally be parenthesized when needed, as in `({} or [])`.
Or-patterns can optionally be parenthesized when needed, as in `({..} or [..])`.

#### Pattern guards

A pattern guard is an optional boolean condition which is attached to a pattern
in a match expression. Note that pattern guards aren't allowed for
destructuring, and they can't appear nested in a larger pattern. A guard is
introduced by the `if` keyword, as in `<pattern> if <condition>`. The condition
is a Nickel expression which can use the variables bound by the pattern.
in a match expression. Note that pattern guards aren't allowed for destructuring
and they can't appear nested in a larger pattern. A guard is introduced by the
`if` keyword, as in `<pattern> if <condition>`. The condition is a Nickel
expression which can use the variables bound by the pattern and must evaluate to
a boolean.

For example, `{tag = _, value = 'Wrapped x} if std.is_number x && x > 0` is a
valid guarded pattern. This pattern will match `{tag = 'Cut, value = 'Wrapped
Expand All @@ -793,7 +796,7 @@ valid guarded pattern. This pattern will match `{tag = 'Cut, value = 'Wrapped
### Match expressions

A match expression is a control flow construct which checks a value against one
or more patterns. A successful match binds the pattern variables to the
or more patterns. The first successful match binds the pattern variables to the
corresponding constituent parts. When applicable, match expressions can
succinctly and advantageously replace long or complex sequences of if-then-else.

Expand Down Expand Up @@ -857,8 +860,8 @@ Examples:

### Destructuring

Destructuring is an extension of the basic binding mechanisms to deconstruct a
structured value.
Destructuring is an extension of the basic let-binding mechanism to deconstruct
a structured value.

Destructuring can take place on a let binding with the form `let <pat> = value
in <exp>` or at a function declaration with the form `fun <pat1> .. <patn> =>
Expand All @@ -870,6 +873,15 @@ is, `let <pat> = value in <exp>` is equivalent to `value |> match { <pat> =>
<exp>`. If the pattern doesn't match the value, an unmatched pattern error is
raised.

Destructuring function arguments requires additional parentheses for enum
patterns and or-patterns. Indeed, `fun 'Foo x => <body>` might be ambiguous: it
can be either a function of one argument expecting a value of the form `'Foo x`,
that is an enum variant with an enum tag as an argument, or a function of two
arguments expecting the first one to be the enum tag `'Foo`. To avoid the
confusion, enum variant patterns and or-patterns must be parenthesized in
argument position. That is, `fun 'Foo x` is thus a function of two arguments and
`fun ('Foo x) => <body>` is a function of one argument.

Examples:

```nickel #repl
Expand Down
4 changes: 4 additions & 0 deletions doc/manual/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,7 @@ keyword, this field must be set in the final configuration.
The second part tells us that in the first record in the users list, the field
`name` has no value while it should have one. This is to be expected as we
removed it earlier.

From Nickel 1.5 and higher, if you are using the Nickel Language Server, you
should even see this contract violation being reported in your editor as you
type.
2 changes: 1 addition & 1 deletion doc/manual/types-vs-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ What to do depends on the context:
local to a file, if your function is bound to a variable, it can be
potentially reused in different places.

Example: `let append_tm: String -> String = fun s => s ++ "(TM)" in ...`
Example: `let append_tm: String -> String = fun s => "%{s} (TM)" in ...`

- *Let-bound function inside a typed block: nothing or type annotation*. Inside
a typed block, types are inferred, so it is OK for simple functions to not
Expand Down
6 changes: 2 additions & 4 deletions examples/arrays/arrays.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ let my_array_lib = {
if arr == [] then
[]
else
let head = std.array.first arr in
let tail = std.array.drop_first arr in
let [head, ..tail] = arr in
[f head] @ map f tail,

fold : forall a b. (a -> b -> b) -> b -> Array a -> b
= fun f first arr =>
if arr == [] then
first
else
let head = std.array.first arr in
let tail = std.array.drop_first arr in
let [head, ..tail] = arr in
f head (fold f first tail),
}
in
Expand Down
49 changes: 25 additions & 24 deletions examples/config-gcc/config-gcc.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,29 @@
# Validate and normalize gcc flags. They can be either a string `-Wextra` or
# a structured value `{flag = "W", arg = "extra"}`. Arguments are not checked.
let GccFlag =
# We only allow the following flags
let available = ["W", "c", "S", "e", "o"] in
let supported_flags = ["W", "c", "S", "e", "o"] in
let is_valid_flag
| doc "check if a string of length > 0 is a valid flag"
= fun string =>
std.array.elem (std.string.substring 0 1 string) supported_flags
in

fun label value =>
std.typeof value
|> match {
'String =>
if std.string.length value > 0
&& std.array.any ((==) (std.string.substring 0 1 value)) available then
value
else
std.contract.blame_with_message "unknown flag %{value}" label,
'Record =>
if std.record.has_field "flag" value && std.record.has_field "arg" value then
if std.array.any ((==) value.flag) available then
#Normalize the tag to a string
value.flag ++ value.arg
else
std.contract.blame_with_message "unknown flag %{value.flag}" label
else
std.contract.blame_with_message
"bad record structure: missing field `flag` or `arg`"
label,
value if std.is_string value && is_valid_flag value =>
value,
{flag, arg} if std.array.elem flag supported_flags =>
#Normalize the tag to a string
"%{flag}%{arg}",
value if std.is_string value =>
std.contract.blame_with_message "unknown flag %{value}" label,
{flag, arg = _} =>
std.contract.blame_with_message "unknown flag %{flag}" label,
{..} =>
std.contract.blame_with_message
"bad record structure: missing field `flag` or `arg`"
label,
_ => std.contract.blame_with_message "expected record or string" label,
}
in
Expand All @@ -51,11 +52,11 @@ let SharedObjectFile = fun label value =>
std.contract.blame_with_message "not a string" label
in

let OptLevel = fun label value =>
if value == 0 || value == 1 || value == 2 then
value
else
std.contract.blame label
let OptLevel = fun label =>
match {
value @ (0 or 1 or 2) => value,
_ => std.contract.blame label,
}
in

let Contract = {
Expand Down
17 changes: 8 additions & 9 deletions examples/fibonacci/fibonacci.ncl
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# test = 'pass'

# This is the naive, exponential version of fibonacci: don't call it on a big
# value!
let rec fibonacci = fun n =>
if n == 0 then
0
else if n == 1 then
1
else
fibonacci (n - 1) + fibonacci (n - 2)
# This is the naive, exponential version of fibonacci: don't call it on a large
# number!
let rec fibonacci =
match {
0 => 0,
1 => 1,
n => fibonacci (n - 1) + fibonacci (n - 2),
}
in
fibonacci 10
6 changes: 3 additions & 3 deletions examples/polymorphism/polymorphism.ncl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# test = 'pass'

# First projection, statically typed
let fst : forall a b. a -> b -> a = fun x y => x in
let first : forall a b. a -> b -> a = fun x y => x in
# Evaluation function, statically typed
let ev : forall a b. (a -> b) -> a -> b = fun f x => f x in
let eval : forall a b. (a -> b) -> a -> b = fun f x => f x in
let id : forall a. a -> a = fun x => x in
(ev id (fst 5 10) == 5 : Bool)
(eval id (first 5 10) == 5 : Bool)
4 changes: 4 additions & 0 deletions examples/record-contract/record-contract.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
# Kubernetes configuration.
# Schema and example derived from
# https://github.com/kubernetes/examples/blob/master/guestbook-go/guestbook-controller.json.
#
# This example is illustrative. If you actually want to use Nickel with
# Kubernetes, consider using the auto-generated contracts from
# https://github.com/tweag/nickel-kubernetes/ instead
let Port | doc "A contract for a port number"
=
std.contract.from_predicate
Expand Down
2 changes: 2 additions & 0 deletions examples/simple-contracts/simple-contract-div.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ let Even = fun label value =>
else
std.contract.blame label
in

let DivBy3 = fun label value =>
if std.is_number value && value % 3 == 0 then
value
else
std.contract.blame label
in

# Will cause an error! 4 is not divisible by 3.
(
4
Expand Down

0 comments on commit e4abda6

Please sign in to comment.