1
1
mirror of https://github.com/tweag/nickel.git synced 2024-11-10 10:46:49 +03:00
nickel/doc/manual/types-vs-contracts.md

98 lines
3.5 KiB
Markdown
Raw Normal View History

# Type vs contracts: when to?
You are writing Nickel code and wonder how it should be annotated. Leave it
2021-12-07 19:20:29 +03:00
alone? Add type annotations? Use contracts? Here is a quick guide when you don't
know what to do!
2021-12-07 19:20:29 +03:00
What is the nature of the expression you are considering?
## A function
2021-12-07 19:20:29 +03:00
Most of the time, you should use **type annotations** for functions. For
reusable code, typing is often more adapted than contracts. You can
exceptionally use contracts when types are not expressive enough to encode the
property you want (such as in `#ValidUrl -> #Port -> #ValidUrl`) or if the type
system is not powerful enough to see that your code is correct.
What to do depends on the context:
2021-12-07 19:20:29 +03:00
- *Anonymous function: nothing*. Short, anonymous functions can
usually live without annotation. Inside a typed block, they will be
2021-12-07 19:20:29 +03:00
typechecked anyway. Outside, anonymous function can't be reused elsewhere,
and are generally short functions passed as arguments to typed higher-order
functions, which will apply a guarding contract.
Example: `lists.map (fun x => x + 1) [1,2,3]`
- *Let-bound function outside of typed block: use a type annotation.* Even if
2021-12-07 19:20:29 +03:00
local to a file, if your function is bound to a variable, it can be
potentially reused in different places.
Example: `let appendTm: Str -> Str = 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 be
annotated. However, you are required to annotate it if it is polymorphic,
because the typechecker won't infer polymorphic types for you. When the
function type is non trivial, it can also be better to write an annotation
for the sake of clarity.
Example:
```nickel
let foo : Num =
let addTwo = fun x => x + 2 in
addTwo 4
2021-12-07 19:20:29 +03:00
in ...
let foo : Num =
let ev : ((Num -> Num) -> Num) -> Num -> Num
= fun f x => f (functions.const x) in
ev (fun f => f 0) 1
in ...
```
## Data (record, list)
Conversely, for data inside configuration code, you should use **contracts**.
2021-12-07 19:20:29 +03:00
Types are not adding much for configuration data, while contracts are more
flexible and expressive.
Example:
```nickel
let Schema = {
name | Str
| doc "Name of the package",
version | #PkgVersion
| doc "The semantic version of the package",
| default = "1.0.0",
build | List #BuildSteps
| doc "The steps to perform in order to build the package",
| default = [],
} in
{
name = "hello",
build = [
command "gcc hello.c -o hello",
command "mv hello $out"
2021-12-07 19:20:29 +03:00
],
} | #Schema
```
## Computation (compound expressions)
Some expressions are neither immediate data nor functions. Take for example the
2021-12-06 23:33:52 +03:00
function application `lists.map (fun s => "http://#{s}/index") servers`.
Usually, you should do **nothing**.
- *Inside configuration: nothing*. The function or operator you are using should
be typed, and thus protected by a contract. The final value should also be
protected by a contract, as per the advice on configuration code. Thus, for a
simple computation like the example above, it is not necessary to add an
annotation.
- *Inside typed block: nothing*. Inside a typed block, the application will be
inferred and typechecked, so you shouldn't have to add anything.
## Debugging
2021-12-06 23:33:52 +03:00
At last, both type annotations and contracts come in handy for debugging. In
this case, you don't have to follow the previous advices, and you can drop
2021-12-07 19:20:29 +03:00
random annotations or contract applications pretty much everywhere you see fit.