From 6d8e9db0e9441c27ef0bf1ca4ccd9e0333490eff Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 May 2024 18:25:47 +0200 Subject: [PATCH] Add documentation for extended patterns 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. --- doc/manual/contracts.md | 14 ++-- doc/manual/correctness.md | 8 ++- doc/manual/merging.md | 2 +- doc/manual/syntax.md | 68 +++++++++++-------- doc/manual/tutorial.md | 4 ++ doc/manual/types-vs-contracts.md | 2 +- examples/arrays/arrays.ncl | 6 +- examples/config-gcc/config-gcc.ncl | 49 ++++++------- examples/fibonacci/fibonacci.ncl | 17 +++-- examples/polymorphism/polymorphism.ncl | 6 +- examples/record-contract/record-contract.ncl | 4 ++ .../simple-contracts/simple-contract-div.ncl | 2 + 12 files changed, 103 insertions(+), 79 deletions(-) diff --git a/doc/manual/contracts.md b/doc/manual/contracts.md index 877208577f..cf0407f85b 100644 --- a/doc/manual/contracts.md +++ b/doc/manual/contracts.md @@ -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, + } } ``` diff --git a/doc/manual/correctness.md b/doc/manual/correctness.md index f7b28d1902..577b419c4d 100644 --- a/doc/manual/correctness.md +++ b/doc/manual/correctness.md @@ -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 @@ -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 diff --git a/doc/manual/merging.md b/doc/manual/merging.md index 432f47a331..b791ed3ed4 100644 --- a/doc/manual/merging.md +++ b/doc/manual/merging.md @@ -611,7 +611,7 @@ argument), we do get a contract violation error: { foo | FooContract } & { foo.required_field1 = "here" } in - + intermediate & { foo.required_field2 = "here" } |> std.deep_seq intermediate diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md index 35b2c4e775..1b36c56a51 100644 --- a/doc/manual/syntax.md +++ b/doc/manual/syntax.md @@ -666,10 +666,10 @@ same language of patterns, described in the following section. A pattern starts with an optional alias of the form ` @ `. 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, @@ -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 @@ -706,11 +706,11 @@ literals. A record pattern is a list of field patterns enclosed into braces of the form `{ , .., , }`. A field pattern is of the form ` = `, where `` is a -sub-pattern matching the content of the field. For example, `foo=bar` and -`foo='Ok value` are valid field patterns. The `= ` 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 `= ` part can be omitted when `` 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 `` can include either: @@ -722,22 +722,24 @@ The optional annotation `` 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 `` part is either an ellipsis `..` or a capture `..`. 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 @@ -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 ` if `. 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 ` if `. 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 @@ -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. @@ -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 = value in ` or at a function declaration with the form `fun .. => @@ -870,6 +873,15 @@ is, `let = value in ` is equivalent to `value |> match { => `. 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 => ` 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) => ` is a function of one argument. + Examples: ```nickel #repl diff --git a/doc/manual/tutorial.md b/doc/manual/tutorial.md index fa3efbba38..a568bdfcec 100644 --- a/doc/manual/tutorial.md +++ b/doc/manual/tutorial.md @@ -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. diff --git a/doc/manual/types-vs-contracts.md b/doc/manual/types-vs-contracts.md index b1c69f9b35..ea291fce06 100644 --- a/doc/manual/types-vs-contracts.md +++ b/doc/manual/types-vs-contracts.md @@ -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 diff --git a/examples/arrays/arrays.ncl b/examples/arrays/arrays.ncl index 66978ba682..81886aaea0 100644 --- a/examples/arrays/arrays.ncl +++ b/examples/arrays/arrays.ncl @@ -7,16 +7,14 @@ 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 diff --git a/examples/config-gcc/config-gcc.ncl b/examples/config-gcc/config-gcc.ncl index bc4a5ab8d1..e9a48b6550 100644 --- a/examples/config-gcc/config-gcc.ncl +++ b/examples/config-gcc/config-gcc.ncl @@ -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 @@ -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 = { diff --git a/examples/fibonacci/fibonacci.ncl b/examples/fibonacci/fibonacci.ncl index f2cd04798c..20e8428eed 100644 --- a/examples/fibonacci/fibonacci.ncl +++ b/examples/fibonacci/fibonacci.ncl @@ -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 diff --git a/examples/polymorphism/polymorphism.ncl b/examples/polymorphism/polymorphism.ncl index 85d452ca01..330533c73e 100644 --- a/examples/polymorphism/polymorphism.ncl +++ b/examples/polymorphism/polymorphism.ncl @@ -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) diff --git a/examples/record-contract/record-contract.ncl b/examples/record-contract/record-contract.ncl index e71c2e3174..ff888d09bc 100644 --- a/examples/record-contract/record-contract.ncl +++ b/examples/record-contract/record-contract.ncl @@ -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 diff --git a/examples/simple-contracts/simple-contract-div.ncl b/examples/simple-contracts/simple-contract-div.ncl index 4d7373f2c0..e2ab82f2dd 100644 --- a/examples/simple-contracts/simple-contract-div.ncl +++ b/examples/simple-contracts/simple-contract-div.ncl @@ -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