1
1
mirror of https://github.com/tweag/nickel.git synced 2024-09-20 16:08:14 +03:00

Update type vs contracts guide

This commit is contained in:
Yann Hamdaoui 2021-12-07 17:20:29 +01:00
parent ce233e725e
commit 80c3335447

View File

@ -1,37 +1,37 @@
# Type vs contracts: when to? # Type vs contracts: when to?
You are writing Nickel code and wonder how it should be annotated. Leave it You are writing Nickel code and wonder how it should be annotated. Leave it
without annotation ? Add type annotations? Use contracts? Here is a quick guide alone? Add type annotations? Use contracts? Here is a quick guide when you don't
when you don't know what to do! know what to do!
If your expression is: What is the nature of the expression you are considering?
## A function ## A function
Most of the time, you should use **type annotations** for functions. Typing is Most of the time, you should use **type annotations** for functions. For
most often more adapted than contracts for functions. You can exceptionally reusable code, typing is often more adapted than contracts. You can
use contracts when types are not expressive enough to encode the property you exceptionally use contracts when types are not expressive enough to encode the
want (as in `#ValidUrl -> #Port -> #ValidUrl`) or if the type system is not property you want (such as in `#ValidUrl -> #Port -> #ValidUrl`) or if the type
powerful enough to see that the code is fine. system is not powerful enough to see that your code is correct.
Type annotations do have a runtime cost. If you hit contracts-related Type annotations do have a runtime cost. If you hit contracts-related
performances issues, you can always disable some contract checks using specific performances issues, you can always disable some contract checks using specific
flags. With annotations, code is still typechecked, and you can still turn flags (TO BE PRECISED ONCE WE HAVE THOSE FLAGS). With annotations, code is still
contracts checking back on for debugging mean when a problem arises. Without typechecked, and you can turn contracts checking back on for debugging. Without
annotations, you're out of luck. annotations, you're out of luck.
What to do depends on the context: What to do depends on the context:
- *Anonymous function: nothing*. Short functions outsife of a let-binding can - *Anonymous function: nothing*. Short, anonymous functions can
usually live without annotation. Inside a typed block, they will be usually live without annotation. Inside a typed block, they will be
typechecked anyway. Outside, they won't be reused elsewhere, and are typechecked anyway. Outside, anonymous function can't be reused elsewhere,
generally small functions passed as arguments to others typed higher-order and are generally short functions passed as arguments to typed higher-order
functions, that will apply a guarding contract. functions, which will apply a guarding contract.
Example: `lists.map (fun x => x + 1) [1,2,3]` Example: `lists.map (fun x => x + 1) [1,2,3]`
- *Let-bound function outside of typed block: use a type annotation.* Even if - *Let-bound function outside of typed block: use a type annotation.* Even if
local to a file, if your function is bound to a variable, it means it can local to a file, if your function is bound to a variable, it can be
be potentially used in several places. potentially reused in different places.
Example: `let appendTm: Str -> Str = fun s => s ++ "(TM)" in ...` Example: `let appendTm: Str -> Str = fun s => s ++ "(TM)" in ...`
- *Let-bound function inside a typed block: nothing or type annotation*. Inside a - *Let-bound function inside a typed block: nothing or type annotation*. Inside a
@ -46,16 +46,23 @@ What to do depends on the context:
let foo : Num = let foo : Num =
let addTwo = fun x => x + 2 in let addTwo = fun x => x + 2 in
addTwo 4 addTwo 4
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 ...
``` ```
## Library (record of functions) ## Library (record of functions)
You should use **type annoations** for records of functions. Currently Nickel You should use **type annoations** for records of functions. Currently Nickel
doesn't have a specific notion of a library or a module. You can just put doesn't have a specific notion of a library or a module. You can just put
functions inside a record. Given the function case, you should also use a type functions inside a record. In accordance with the previous section, you should
annotation on your record to make the type of the functions accessible to the also use a type annotation on your record to make the type of the functions
outside. Otherwise, the record is typed as `Dyn` and will shadow the type accessible to the outside. Otherwise, the record is typed as `Dyn` and will
information, making your library basically unusable inside typed code. obliterate the types, making your library basically unusable inside typed code.
### Example ### Example
@ -78,11 +85,12 @@ BUT DO
} }
``` ```
Alternatively, you can repeat your types both at the function level Alternatively, you can repeat your types both at the function level and at the
and at the record level. It makes code more navigable and `query`-friendly, but at the expense of record level. It makes code more navigable and `query`-friendly, but at the
repetition and duplicated contract checks. It is also currently required for expense of repetition and duplicated contract checks. It is also currently
polymorphic functions because of [the following bug](). A better solution will probably be required for polymorphic functions because of [the following
implemented in the future: type holes. bug](https://github.com/tweag/nickel/issues/360). A better solution will
probably be implemented in the future: type holes (TODO: MAY ACTUALLY BE AVAILABLE FOR RELEASE)
(NOT YET POSSIBLE) (NOT YET POSSIBLE)
``` ```
@ -95,8 +103,8 @@ implemented in the future: type holes.
## Data (record, list) ## Data (record, list)
Conversely, for data inside configuration code, you should use **contracts**. Conversely, for data inside configuration code, you should use **contracts**.
Types are not adding much over contracts for configuration data, while contracts Types are not adding much for configuration data, while contracts are more
are more flexible and expressive. flexible and expressive.
Example: Example:
```nickel ```nickel
@ -115,7 +123,7 @@ let Schema = {
build = [ build = [
command "gcc hello.c -o hello", command "gcc hello.c -o hello",
command "mv hello $out" command "mv hello $out"
] ],
} | #Schema } | #Schema
``` ```
@ -137,5 +145,4 @@ Usually, you should do **nothing**.
At last, both type annotations and contracts come in handy for debugging. In 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 this case, you don't have to follow the previous advices, and you can drop
random annotations or contract applications pretty much everywhere you see fit random annotations or contract applications pretty much everywhere you see fit.
to help better locate when something is going wrong.