Skip to content

Instantly share code, notes, and snippets.

@Lorenzobattistela
Last active September 1, 2023 14:12
Show Gist options
  • Save Lorenzobattistela/206f9c6eb15d63bb3e71585dd2d1672e to your computer and use it in GitHub Desktop.
Save Lorenzobattistela/206f9c6eb15d63bb3e71585dd2d1672e to your computer and use it in GitHub Desktop.
ocaml-syntax
(* Lists and Patterns *)
(* An OCaml list is an immutable finite sequence of elements of the same type. *)
open Base;
let list = [1;2;3]
let list = 1 :: 2 :: 3 :: []
(* The way in which the :: operator attaches elements to the front of a list reflects the fact that ocaml lists are in fact signly linked lists. *)
(* We can read data out of a list using match expressions *)
let rec sum l =
match l with
| [] -> 0
| hd :: tl -> hd + sum tl
sum [1;2;3;4]
- : int = 10
(* drop value in a list *)
let rec drop_value l to_drop =
match l with
| [] -> []
| hd :: tl ->
let new_tl = drop_value tl to_drop in
if hd = to_drop then new_tl else hd :: new_tl
drop_value [1;2;3] 2
- : int list = [1;3]
(* Limitations and blessings of pattern matching *)
(* The preceding example highlights an important fact about patterns, which is that they can’t be used to express arbitrary conditions. Patterns can characterize the layout of a data structure and can even include literals, as in the drop_zero example, but that’s where they stop. A pattern can check if a list has two elements, but it can’t check if the first two elements are equal to each other. *)
(* You can think of patterns as a specialized sublanguage that can express a limited (though still quite rich) set of conditions. The fact that the pattern language is limited turns out to be a good thing, making it possible to build better support for patterns in the compiler. In particular, both the efficiency of match expressions and the ability of the compiler to detect errors in matches depend on the constrained nature of patterns. *)
(* pattern matching is usually faster than ifs or other thing we might right ourselves *)
(* another good point of pattern matching is the capability of error-detecting. *)
(* Using the list module effectively *)
let header = ["language";"architect";"first release"]
let data = [
["Lisp" ;"John McCarthy" ;"1958"] ;
["C" ;"Dennis Ritchie";"1969"] ;
["ML" ;"Robin Milner" ;"1973"] ;
["OCaml";"Xavier Leroy" ;"1996"] ;
]
let max_widths header rows =
let lengths l = List.map ~f:String.length l in
List.fold rows
~init:(lengths header)
~f:(fun acc row ->
List.map2_exn ~f:Int.max acc (lengths row));;
let render_separator widths =
let pieces = List.map widths
~f:(fun w -> String.make w '-')
in
"|-" ^ String.concat ~sep:"-+-" pieces ^ "-|";;
(* Tail recursion *)
(* The only way to compute the length of a list in ocaml is to walk up from start to end. This is O(n), so it gets bad on
large list size.
*)
let rec length = function
| [] -> 0
| _ :: tl -> 1 + length tl;;
(* But this causes stack overflow for long lists, but we could try this alternative version *)
let rec length_plus_n l n =
match l with
| [] -> n
| _ :: tl -> length_plus_n tl (n + 1);;
let length l = length_plus_n l 0;;
open Base
open Stdio
(* This is a recursive function that reads from input at each iteration for accumulated sum. *)
let rec read_and_accumulate accum =
let line = In_channel.input_line In_channel.stdin in
match line with
| None -> accum
| Some x -> read_and_accumulate (accum +. Float.of_string x)
let () =
printf "Total: %F\n" (read_and_accumulate 0.)
(* To compile and run, create a dune fie *)
(lang dune 2.9)
(name rwo-example)
(executable)
(name sum)
(libraries base stdio))
(* and invoke with dune build sum.exe *)
(* $ ./_build/default/sum.exe
1
2
3
94.5
Total: 100.5 *)
let x = 3;; (* just at utop the ;; *)
let y = 3.;; (* float *)
let z = y +. 4.;; (* float operators should have . (+., -. etc)*)
let x = "string";;
let y = [1;2;3];; (* list of ints *)
let y = 1 :: 2 :: 3 :: [];; (* same list of ints *)
let tp = (1,2,3);; (* create tuple *)
let tp = 1,2,3;; (* same tuple *)
let (x, y, z) = tp;; (*unpacking tuple*)
let square x = x * x;; (* function square *)
(* dist function using unpacking *)List.map languages ~f:String.length;;
- : int list = [5; 4; 1]
let distance (x1, x2) (y1, y2) = List.map languages ~f:String.length;;
- : int list = [5; 4; 1]
Float.sqrt((x1 -. x2) **. 2. +. (y1 -. y2) **. 2.);;List.map languages ~f:String.length;;
- : int list = [5; 4; 1]
(* length of the list *)
let list = [1;2;3];;
List.length list;;
let languages = ["ocaml";"c"];
List.map languages ~f:String.length;;
(* create a match pattern for our list. if our list is passed, the first element is returned, if the list is empty we have a default value*)
let fav_lang languages =
match languages with
| first :: the_rest -> first
| [] -> "Default val"
let rec sum l =
match l with
| [] -> 0 (* base case *)
| hd :: tl -> hd + sum tl (* inductive case *);;
sum [1;2;3];; (* the sum of all nums in the list *)
(* note that HD is the head of the list and TL is the tail *)
(* example factorial passing [3,2,1] for example *)
let rec factorial l =
match l with
| [1] -> 1
| hd :: tl -> hd * factorial tl
| [] -> 0;;
let rec remove_sequential_duplicates list =
match list with
| [] -> []
| [x] -> [x]
| first :: second :: tl ->
if first = second then
remove_sequential_duplicates (second :: tl)
else
first :: remove_sequential_duplicates (second :: tl);;
remove_sequential_duplicates [1;1;2;3;3;4;4;1;1;1];;
(* [1;2;3;4] *)
(* options: The function divide either returns None if the divisor is zero, or Some of the result of the division otherwise.
Some and None are constructors that let you build optional values, just as :: and [] let you build lists.
You can think of an option as a specialized list that can only have zero or one elements.
*)
let divide x y =
if y = 0 then None else Some (x / y);;
let downcase_extension filename =
match String.rsplit2 filename ~on:'.' with (*split the string at the . char*)
| None -> filename (* if nothing after the . just return the filename *)
| Some (base,ext) -> (* since the split return the base and the extension, we return the base, get them together with . and lowercase *)
base ^ "." ^ String.lowercase ext;;
(* ^ is concatenation operator *)
(* here we map each extension in our list with our new fn *)
List.map ~f:downcase_extension
[ "Hello_World.TXT"; "Hello_World.txt"; "Hello_World" ];;
(* creating new data types *)
type point2d = { x : float; y : float; };;
let p = { x = 3.; y = -4. };;
(* val p : point2d = {x = 3.; y = -4.} *)
(* we can access these types using pattern matching *)
let magnitude { x; y } = Float.sqrt (x **. 2. +. y **. 2.);;
(* val magnitude : point2d -> float = <fun> *)
let distance v1 v2 =
magnitude { x = v1.x -. v2.x; y = v1.y -. v2.y };;
(* we can compose larger types *)
type circle_desc = { center: point2d; radius: float }
type rect_desc = { lower_left: point2d; width: float; height: float }
type segment_desc = { endpoint1: point2d; endpoint2: point2d }
(* We also have Variant types, that we can combine multiple objects of the other types as a description of a multi
- object scene, a unified way of representing those object *)
type scene_el =
| Circle of circle_desc
| Rect of rect_desc
| Segment of segment_desc
(* Now we can write a function to test wether a point is in the interior of some element of a list of scene_els.*)
let is_inside_scene_element point scene_element =
let open Float.O in
match scene_element with
| Circle { center; radius } ->
distance center point < radius
| Rect { lower_left; width; height } ->
point.x > lower_left.x && point.x < lower_left.x + width
&& point.y > lower_left.y && point.y < lower_left.y + height
| Segment _ -> false
(* here we use an anonymous function (List.exists) and the purpose of it is to check if there are any
element in the list in question for which the provided function evaluates to true (in this case, to check if
there is a scene element within which our point resides) *)
let is_inside_scene point scene =
List.exists scene
~f:(fun el -> is_inside_scene_element point el);;
is_inside_scene {x=3.;y=7.} [ Circle {center = {x=4.;y= 4.}; radius = 0.5 } ];;
(* Imperative programming - arrays, mutable data struct *)
let numbers = [| 1; 2; 3; 4 |];;
numbers.(2) <- 4;;
numbers
- : int array = [|1; 2; 4; 4|]
(* mutable record fields -> storing a running statistical summary of a collection of nums *)
type running_sum =
{
mutable sum: float;
mutable sum_sq: float;
mutable samples: int;
}
let mean rsum = rsum.sum /. Float.of_int rsum.samples;;
let stdev rsum = Float.sqrt (rsum.sum_sq /. Float.of_int rsum.samples -. mean rsum **. 2.);;
(*create returns a running_sum corresponding to the empty set, and update rsum x changes rsum to
reflect the addition of x to its set of samples by updating the number of samples, the sum, and the sum of squares.*)
let create () = { sum = 0.; sum_sq = 0.; samples = 0 };;
let upate rsum x =
rsum.samples <- rsum.samples + 1;
rsum.sum <- rsum.sum +. x;
rsum.sum_sq <- rsum.sum_sq +. x *. x;;
let rsum = create ();;
List.iter [1.;3.;2.;-7.;4.;5.] ~f: (fun x -> update rsum x);;
(* refs: we can create a single mutable value using a ref - which is a record type with a single mutable field
called contents *)
let x = { contents = 0 };;
x.contents <- x.contents + 1;;
(* and we have useful functions for refs *)
let x = ref 0
!x (* same as x.contents *)
x := !x + 1 (* assignment *)
(* no magic, refs are simply implemented like: *)
type 'a ref = { mutable contents : 'a };;
let ref x = { contents = x };;
let (!) r = r.contents;;
let (:=) r x = r.contents <- x;;
(* nesting lets with LET and IN. A let paired with an in can be used to introduce a new binding within any local scope
including a function body. The in marks the beginning of the scope within which the new variable can be used. *)
let z = 7 in
z + z;; (* and the scope of the let binding is terminated by ;; *)
(* we can have multiple bindings, each one adding a new var binding to what came before *)
let x = 7 in
let y = x * x in
x + y;;
(* we can also use for and while loops *)
let permute array =
let length = Array.length array in
for i = 0 to length - 2 do
(* pick a j to swap with *)
let j = i + Random.int (length - i) in
(* Swap i and j *)
let tmp = array.(i) in
array.(i) <- array.(j);
array.(j) <- tmp
done;;
let ar = Array.init 20 ~f:(fun i -> i);;
permute ar;;
ar;;
(* As a side note, the preceding code takes advantage of the fact that &&, OCaml’s “and” operator, short-circuits.
In particular, in an expression of the form expr1&&expr2, expr2 will only be evaluated if expr1 evaluated to true.
Were it not for that, then the preceding function would result in an out-of-bounds error.*)
let find_first_negative_entry array =
let pos = ref 0 in
while
let pos_is_good = !pos < Array.length array in
let element_is_non_negative = array.(!pos) >= 0 in
pos_is_good && element_is_non_negative
do
pos := !pos + 1
done;
if !pos = Array.length array then None else Some !pos;;
find_first_negative_entry [|1;2;0;3|];;
(* Exception: Invalid_argument "index out of bounds". *)
(* Variables are defined as: let <variable> = <expr>
Every variable binding has a scope, which is the portion of the code that can refer to that binding.
let can also be used to create a variable binding whose scope is limited to a particular expression, using the following syntax:
let <variable> = <expr1> in <expr2>
This first evaluates expr1 and then evaluates expr2 with variable bound to whatever value was produced by the evaluation of expr1.
In practice: *)
let languages = "OCaml,Perl,C++,C";;
let dashed_languages =
let language_list = String.split languages ~on:',' in
String.concat ~sep:"-" language_list;;
(* val dashed_languages : string = "OCaml-Perl-C++-C" *)
(* Note that the scope of language_list is just the expression String.concat ~sep:"-" language_list and
is not available at the toplevel. *)
(* A let binding in an inner scope can shadow, or hide the definition from an outer scope. So, for example, we could have
written the dashed_languages as follows: *)
let languages = "Ocaml,Perl,C";;
let dashed_languages =
let languages = Stirng.split languages ~on: ',' in
String.concat ~sep:"-" languages;;
(* This time in the inner scope we called the list of strings languages instead of language_list, this
hiding the original definition of languages. But once the definition of dashed_languages is complete, the
inner scope has closed and the original definition of languages is still available. *)
(* One common idiom is to use a series of nested let/in expressions to build up the components of a larger computation. *)
let area_of_ring inner_radius outer_radius =
let pi = Float.pi in
let area_of_circle r = pi *. r *. r in
area_of_circle outer_radius -. area_of_circle inner_radius;;
area_of_ring 1. 3.;;
(* if we did: *)
let area_of_ring inner_radius outer_radius =
let pi = Float.pi in
let area_of_circle r = pi *. r *. r in
let pi = 0. in
area_of_circle outer_radius -. area_of_circle inner_radius;;
(* The program would still work as expected, because pi was only shadowed, and the new value would be used
just if we used it again. *)
(* In ocaml let bindings are immutable. There are many kinds of mutable values, but no mutable variables *)
(* Pattern matching and let *)
let (ints,strings) = List.unzip [(1,"one"); (2,"two"); (3,"three")];;
(* (ints,strings) is a pattern, and the let binding assigns values to both of the iedntifiers that show up in that pattern.
A pattern is essentially a description of the shape of a data structurem where some components are names to be bound to values.
*)
(*
Using a pattern in a let binding makes the most sense for a pattern that is irrefutable, i.e.,
where any value of the type in question is guaranteed to match the pattern. Tuple and record patterns are irrefutable,
but list patterns are not. Consider the following code that implements a function for upper casing the first element
of a comma-separated list. *)
let upcase_first_entry line =
let (first :: rest) = String.split ~on:',' line in
String.concat ~sep:"," (String.uppercase first :: rest);;
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
(* In practice it would not happen because split alwayus return a list with at least one element, but the compiler doesnt know. *)
(* So its generally better to use a match expression to handle such cases: *)
let upcase_first_entry line =
match String.split ~on:',' line with
| [] -> assert false
| first :: rest -> String.concat ~sep:"," (String.uppercase first :: rest);;
(* assert is useful for marking cases that should be impossible *)
(* FUNCTIONS *)
(* We can declare a function without a name. An anonymous function: *)
(fun x -> x + 1);;
(* We can apply them to an argument: *)
(fun x -> x + 1)7;;
(* - : int = 8 *)
(* And we can pass them to another functions. Passing them to iteration funcionts like List.map is probably the most cmmon use case. *)
List.map ~f:(fun x -> x + 1) [1;2;3];;
(* - : int list = [2;3;4] *)
(* you can even stuff a function into a DS like a list: *)
let transforms = [ String.uppercase; String.lowrcase ];;
(* transforms is a list of functions *)
List.map ~f:(fun g -> g "Hello World") transforms;;
- : string list = ["HELLO WORLD"; "hello world"]
(* we can define named functions as *)
let plus_one x = x + 1
(*
Functions and let bindings have a lot to do with each other.
In some sense, you can think of the parameter of a function as a variable being bound to the value passed by the caller.
Indeed, the following two expressions are nearly equivalent
*)
(fun x -> x + 1) 7;;
let x = 7 in x + 1;;
(* We can also have multiarg functions: *)
let abs_diff x y = abs(x - y)
(* and the signature would be: val abs_diff : int -> int -> int = <fun> , but why? *)
(* We can define the same function using fun as: *)
let abs_diff =
(fun x -> (fun y -> abs (x - y)));;
(* This actually explains the signature. In fact, abs_diff is a function with one argument that returns another functionf
of one argument, which itself returns the result. Since the functions are nested, abs(x - y) has acccess to both values.
This style of function is called a curried function. (Currying is named after Haskell Curry, a logician who had
a significant impact on the design and theory of programming languages.) The key to interpreting the type signature
of a curried function is the observation that -> is right-associative. The type signature of abs_diff can therefore be
parenthesized as follows. *)
(* val abs_diff : int -> (int -> int) *)
(* Currying is useful to specialize a function by feeding in some arguments. For example, we could craete a specialized
function that measures the abs diff of x and 3.
*)
let dist_from_3 = abs_diff 3
(* val dist_from_3 : int -> int = <fun> *)
dist_from_3 8
(* 5 *)
(* The practice of applying some of the arguments of a curried function to get a new function is called partial application.*)
(* In OCaml, there is no penalty for calling a curried function with all of its arguments.
(Partial application, unsurprisingly, does have a small extra cost.) *)
(* But Currying is not the only way to create multiargument functions *)
let abs_diff (x,y) = abs (x - y);;
val abs_diff : int * int -> int = <fun>
abs_diff (3,4);;
- : int = 1
(* But in this case you cannot do partial application *)
(* Recursive functions *)
let rec find_first_repeat list =
match list with
| [] | [_] ->
(* only zero or one elements, so no repeats, _ is used so we dont name the element *)
None
| x :: y :: tl -> (* x is the first element, y the second and t1 is the rest of the list *)
if x = y then Some x else find_first_repeat (y::tl);; (* some will return the x element *)
(* We can also define multiple recursive values combining let rec, and: Here is an example (RLLY inefficient btw) *)
let rec is_even x =
if x = 0 then true else is_even(x - 1)
and is_odd x =
if x = 0 then false else is_odd(x - 1)
List.map ~f:is_even [0;1;2;3;4;5];;
- : bool list = [true; false; true; false; true; false]
(* OCaml distinguishes between nonrecursive definitions (using let)
and recursive definitions (using let rec) largely for technical reasons: the type-inference algorithm needs to
know when a set of function definitions are mutually recursive, and these have to be marked explicitly by the programmer. *)
(* But this decision has some good effects. For one thing, recursive (and especially mutually recursive) definitions are harder
to reason about than nonrecursive ones. It’s therefore useful that, in the absence of an explicit rec, you can assume that
a let binding is nonrecursive, and so can only build upon previous definitions. *)
(* In addition, having a nonrecursive form makes it easier to create a new definition that extends and supersedes an
existing one by shadowing it. *)
(* Prefix and Infix Operators *)
Int.max 3 4 (* prefix *);;
3 + 4 (* infix *);;
(* The second one is an ordinary function. In fact, we could rewrite as: *)
(+) 3 4
List.map ~f:((+) 3) [4;5;6];;
(* In this case we partially apply (+) with 3, and apply to every element in the list *)
(* We can define or redefine operators: then we can apply some of two points, for example *)
let (+!) (x1,y1) (x2,y2) = (x1 + x2, y1 + y2);;
(3,2) +! (-2,4);;
(* But careful when creating operators that contain *. Consider the following: *)
let (***) x y = (x **. y) **. y;;
Line 1, characters 18-19:
Error: This expression has type int but an expression was expected of type
float
(* This happens because *** isnt interpreted as an operator at all. Its read as a comment. so to get this to work properly, we need
to put spaces around any operator that begins or ends with *: *)
let ( *** ) x y = (x **. y) **. y;;
val ( *** ) : float -> float -> float = <fun>
(*
If we want to pass a negative value to a function, we need to wrap it in parentheses, because it has lower precedence
than function applications *)
Int.max 3 (-4);;
- : int = 3
(* Here is an example of a very useful operator from the stdlib whose behavior depends critically on the precedence rules just seen *)
let (|>) x f = f x
(*
This is called the reverse application operator, and it’s not quite obvious at first what its purpose is:
it just takes a value and a function and applies the function to the value.
Despite that bland-sounding description, it has the useful role of sequencing operations,
similar in spirit to using the pipe character in the UNIX shell. Consider, for example,
the following code for printing out the unique elements of your PATH. *)
(* dedup and sort removes duplicates and sort based on string comparison *)
open Stdio;;
let path = "/usr/bin:/usr/local/bin:/bin:/sbin:/user/bin";;
String.split ~on:':' path
|> List.dedup_and_sort ~compare:String.compare
|> List.iter ~f:print_endline;;
(* We could do the same using intermediate values, but it is a bit more verbose; *)
let split_path = String.split ~on:':' path in
let deduped_path = List.dedup_and_sort ~compare:String.compare split_path in
List.iter ~f:print_endline deduped_path;;
(*
An important part of what’s happening here is partial application. For example, List.iter takes two arguments:
a function to be called on each element of the list, and the list to iterate over.
We can call List.iter with all its arguments: *)
List.iter ~f:print_endline ["Two"; "lines"];;
(* Or we can pass it just the function argument, leaving us w a function for printing out a list of strings *)
List.iter ~f:print_endline;;
(* It is this later form that we’re using in the preceding |> pipeline. *)
(* But |> only works in the intended way because it is left-associative. Let's see what happens if we try using a right
associative operator, like ^> *)
let (^>) x f = f x;;
String.split ~on:':' path
^> List.dedup_and_sort ~compare:String.compare
^> List.iter ~f:print_endline;;
Line 3, characters 6-32:
Error: This expression has type string list -> unit
but an expression was expected of type
(string list -> string list) -> 'a
Type string list is not compatible with type
string list -> string list
(* The type error is a little bewildering at first glance. What’s going on is that, because ^> is right associative, the operator
is trying to feed the value List.dedup_and_sort ~compare:String.compare to the function
List.iter ~f:print_endline. But List.iter ~f:print_endline expects a list of strings as its input, not a function. *)
(* The application operator *)
(@@);;
- : ('a -> 'b) -> 'a -> 'b = <fun>
(* This one is useful when we want to avoid many parentheses layers when applying functions to complex expressions.
In particular, we can replace f(g(h x)) with f @@ g @@ h x (like in math f o g)
@@ is right associative; *)
let f x = x + 1
let h x = x + 2
let g x = x + 3
f( g( h x))
- : int = 9
f @@ g @@ h x
- : int = 9
(* Declaring functtions with function keyword *)
let some_or_zero = function
| Some x -> x
| None -> 0;;
val some_or_zero : int option -> int = <fun>
List.map ~f:some_or_zero [Some 3; None; Some 4];;
- : int list = [3; 0; 4]
(* Some is a constructor for the option type. None is the other constructor. This is so that both None and Some have the same type. *)
(* And this would be the same thing if we declared with match: *)
let some_or_zero =
match num_opt with:
| Some x -> x
| None -> 0;;
(* We can also combine the different styles of function declaration together, as in the following example, where we declare
a two-argument currie function with a pattern match on the second arg
*)
let some_or_default default = function
| Some x -> x
| None -> default;;
some_or_default 3 (Some 5);;
- : int = 5
List.map ~f:(some_or_default 100) [Some 3; None; Some 4];;
- : int list = [3; 100; 4]
(* Also, note the use of partial application to generate the function passed to List.map.
In other words, some_or_default 100 is a function that was created by feeding just the first argument to some_or_default. *)
(* Labeled Args *)
(* Until now, the functions we've defined specified args positionally. But ocaml also supports labeled args, which let you
identify a function argument by name. Labeled args are marked by a leading tilde and a label. *)
let ratio ~num ~denom = Float.of_int num /. Float.of_int denom
ratio ~num:3 ~denom:10
(* Ocaml also suports label punning, which means you get to drop the text after the colon if the name of the label
and the name of the variable being used are the same. *)
let num = 3 in
let denom = 4 in
ratio ~num ~denom
(* Labels are useful when explicating long argument lists, adding information to uninformative argument types (example:
consider a fn to create a hash table whose first arg is the initial size of the array backing the HT and the second is a
boolean flag, which indicates wether the array will ever shrink when elements are removed:
val create_hashtable : int -> bool -> ('a,'b) Hashtable.t
The signature makes it hard to divine the meaning of the args;
ALso useful for disambiguating similar arguments.
Also provides flexible arg ordering and partial application.
*)
(* Higher order functions and labels *)
(*
While the order doesnt matter when calling a function with labeled args, it matters in a higher order context
e.g when passing a function with labeled args to another function: *)
let apply_to_tuple f (first,second) = f ~first ~second;;
val apply_to_tuple : (first:'a -> second:'b -> 'c) -> 'a * 'b -> 'c = <fun>
(* Here, the definition of apply_to_tuple sets up the expectation that its first argument is a function with two labeled arguments, first and second, listed in
that order. We could have defined apply_to_tuple differently to change the order in which the labeled arguments were listed. *)
let apply_to_tuple_2 f (first,second) = f ~second ~first;;
(* It turns out this order matters. In particular, if we define a function that has a different order: *)
let divide ~first ~second = first / second
(* we'll finde out htat it can be passed to apply_to_tuple_2 *)
(* Error: This expression has type first:int -> second:int -> int
but an expression was expected of type second:'a -> first:'b -> 'c *)
(* But works for the first one: *)
let apply_to_tuple f (first,second) = f ~first ~second;;
val apply_to_tuple : (first:'a -> second:'b -> 'c) -> 'a * 'b -> 'c = <fun>
apply_to_tuple divide (3,4);;
- : int = 0
(* Optional arguments *)
(* An optional arg is like a labeled argument that a caller can choose wther or not to provide. They're passed in
using the same syntax as labeled args, and can be provided in any order. Here's an example of string concat with optional
separator. *)
let concat ?sep x y =
let sep = match sep with None -> "" | Some s -> s in
x ^ sep ^ y;;
concat "foo" "bar" (* without the optional argument *);;
concat ~sep:":" "foo" "bar" (* with the optional argument *);;
(* we can define it better with syntax sugar so we ont need the none boilerplate *)
let concat ?(sep="") x y = x ^ sep ^ y;;
(* Optional args are useful, but easy to abuse. The key advantage is that we can write fns with multiple args that users
can ignore most of the time. They also allow oyu to extend an API with new functionality without changing existing code.
They can also allow you to extend an API with new functionality without changing existing code.
The downside is that the caller may be unaware that there is a choice to be made and wrongly pick the default behavior. *)
(*
A good rule of thumb is to avoid optional arguments for functions internal to a module
, i.e., functions that are not included in the module’s interface, or mli file *)
(* Explicit passing of an optional argument *)
(* Under the covers, a fn with an optional arg receives None when the caller doesnt provide the argument, and Some when it does.
But the Some and None are normally not explicitly passed by the caller. But sometimes pasing in Some or NOne explicitly is
exactly what you want. Then you can do the folowing: *)
concat ~sep:":" "foo" "bar" (* provide the optional argument *);;
- : string = "foo:bar"
concat ?sep:(Some ":") "foo" "bar" (* pass an explicit [Some] *);;
- : string = "foo:bar"
(* and the following two lines are equivalent ways of calling concat without specifying sep. *)
concat "foo" "bar" (* don't provide the optional argument *);;
- : string = "foobar"
concat ?sep:None "foo" "bar" (* explicitly pass `None` *);;
- : string = "foobar"
(* Inference of labeled and optional arguments *)
(* How are labeled arguments inferred by the type system? Consider the following example for computing numerical derivatives of a function of two real variables. The function takes an argument delta, which determines the scale at which to compute the derivative; values x and y, which determine at which point to compute the derivative; and the function f, whose derivative is being computed. The function f itself takes two labeled arguments, x and y. Note that you can use an apostrophe as part of a variable name, so x' and y' are just ordinary variables. *)
let numeric_deriv ~delta ~x ~y ~f =
let x' = x +. delta in
let y' = y +. delta in
let base = f ~x ~y in
let dx = (f ~x:x1 ~y -. base) /. delta in
let dy = (f ~x ~y:y' -. base) /. delta in
(dx,dy);;
val numeric_deriv :
delta:float ->
x:float -> y:float -> f:(x:float -> y:float -> float) -> float * float =
<fun>
(* In principle, it’s not obvious how the order of the arguments to f should be chosen. Since labeled arguments can be passed in arbitrary order, it seems like it could as well be y:float -> x:float -> float as it is x:float -> y:float -> float.
Even worse, it would be perfectly consistent for f to take an optional argument instead of a labeled one, which could lead to this type signature for numeric_deriv. *)
val numeric_deriv :
delta:float ->
x:float -> y:float -> f:(?x:float -> y:float -> float) -> float * float
(* Since there are multiple plausible types to choose from, OCaml needs some heuristic for choosing between them. The heuristic the compiler uses is to prefer labels to options and to choose the order of arguments that shows up in the source code.
Note that these heuristics might at different points in the source suggest different types. Here’s a version of numeric_deriv where different invocations of f list the arguments in different orders. *)
let numeric_deriv ~delta ~x ~y ~f =
let x' = x +. delta in
let y' = y +. delta in
let base = f ~x ~y in
let dx = (f ~y ~x:x' -. base) /. delta in
let dy = (f ~x ~y:y' -. base) /. delta in
(dx,dy);;
Line 5, characters 15-16:
Error: This function is applied to arguments
in an order different from other calls.
This is only allowed when the real type is known.
(* The compiler complains that the order of arguments to f in the second call is different from the order in the first call. This is because the compiler has inferred that f takes labeled arguments, and so it expects the order of arguments to be consistent across calls. *)
let numeric_deriv ~delta ~x ~y ~(f: x:float -> y:float -> float) =
let x' = x +. delta in
let y' = y +. delta in
let base = f ~x ~y in
let dx = (f ~y ~x:x' -. base) /. delta in
let dy = (f ~x ~y:y' -. base) /. delta in
(dx,dy);;
(* but that compiles ok, due to the type annotation on f*)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment