This is a nice bit of code.
Iām donāt like the |> operator. Iāve seen it in some programs and Iāve never liked how it was used.
Going just a bit crazyā¦ Letās write "Hello world!!"
to hello.txt
. Easy
let ( /> ) str filename =
let ch = open_out filename in
Fun.protect
~finally:(fun () -> close_out ch)
(fun () -> output_string ch str)
"Hello, world!" /> "hello.txt"
But that (fun () -> ..)
feels a bit repetitive. Well we can get rid of one.
let ( let$$ ) finally work =
Fun.protect ~finally work
let ( /> ) str filename =
let ch = open_out filename in
let$$ _ = fun () -> close_out ch in
output_string ch str
I thought this code in dune was quite fun:
The module rec
can create the module from the module type automatically, as Ast
has no values.
In the same idea, I like this line:
It permits to redefine the type t
(with an arity) and include the whole implementation to describe an other interface - which supports capabilities. By this way, we donāt need to copy/paste the implementation if we want to improve the interface.
considering your nick, I was sure, youāre a fan of it.
This is especially useful with first class modules as you can do
module rec M : sig
module type Map = sig
type key
(* this is the bad recursive bit *)
val clone : unit -> (module M.Map with type key = key)
end
end = M
This was exactly the reason for the Fun.protect
design, and it was nice that we managed to agree on the design principles since as I remember it, it was not at all clear a priori that a consensus could be reached.
Good exception handling is surprisingly involved, my talk at the OCaml workshop on Friday will talk about it.
This does not look crazy to me. But I thought the OCaml community preferred to tie the resource acquisition to the destructor initialisation using a higher-order function with_file : string -> (in_channel -> 'a) -> 'a
instead of open_in
. Is there a benefit to defer
(i.e. some additional flexibility)?
Something like defer
I would consider a more low-level or on-off approach than a with_file
function, which captures a specific use case.
One of my first OCaml code snippets written a year ago is to get 4 random words from a word-list file without reading it in full and thus having constant complexity with growing list length:
open Bigarray
let rec step arr dx look stop idx =
match idx = stop with
| true -> stop
| _ -> (
match arr.{idx + look} with
| '\n' -> idx
| _ -> step arr dx look stop (idx + dx) )
(* walk back until we hit the begin of the line *)
let back arr idx = step arr (-1) (-1) 0 idx
(* walk forward until we hit the end of the line *)
let forw arr len idx = step arr 1 0 len idx
let line arr len idx =
let off = back arr idx and pos = forw arr len idx in
pos - off |> Array1.sub arr off |> Bigstring.to_string
let () =
Random.self_init ();
let fd = Unix.openfile "words.txt" [ Unix.O_RDONLY ] 0 in
let mf = Unix.map_file fd char c_layout false [| -1 |] in
let arr = array1_of_genarray mf in
let len = Array1.dim arr in
(fun _ -> len |> Random.int |> Lines.line arr len)
|> List.init 4 |> String.concat "-" |> print_endline;
Unix.close fd
See the complete project at mro/xkcd936: āļø Mirror of https://code.mro.name/mro/xkcd936 | š« š Generate word combinations like in xkcd.com/936 - xkcd936 - Codeberg.org
Some more āMinistry of Silly Codeā stuff
let [@inline] fn f x = fun () -> f x
let ( => ) = fn
let ( /> ) str filename =
let ch = open_out filename in
Fun.protect ~finally:(close_out => ch)
(output_string ch => str)
On a more serious note, if more of the Stdlib API will require a thunk as argument (fun () -> .. )
a more
lightweight way of creating them is kind of missing.
Fun.protect
is nice example of where such stuff will be handy, as well as some of the Seq and Option stuff.
I like the idea, but I do not like the operator order. output_string ch => str
is difficult for me to understand (the example is trivia here, but this will blow my mind out of this context)
I didnāt checked if we have composition issues, but this seem more natural for me reversed:
let [@inline] fn x f = fun () -> f x
It will work
let ( /> ) str filename =
let ch = open_out filename in
Fun.protect ~finally:(ch => close_out)
(str => output_string ch)
A prefix operator is also possible. Iām reminded of the lazy operator $
used in Okasakiās PFDS, so we can define an operator ~$
that is some approximation of that:
# let ( ~$ ) f x () = f x ;;
val ( ~$ ) : ('a -> 'b) -> 'a -> unit -> 'b = <fun>
# let f = ~$ Printf.sprintf "delayed print" ;;
val f : unit -> string = <fun>
# f () ;;
- : string = "delayed print"
Iām not sure whether this already exists somewhere, but Iāve never seen it, so here I go.
In the eternal quest of offsetting as many issues from runtime to compile time, Iāve always encoded the fact a list cannot be empty in its type. Historically as something akin to type 'a list_nonempty = 'a * 'a list
, which does the job decently: you get a compile time guarantee that the list has at least a head instead of asserting that List.hd
is not None
at runtime because you ājust know it canāt be emptyā. It also enforces clearly in the signature that an empty list cannot be passed. The drawback being that the type differs from a standard list, so client code must be adapted and becomes a bit more convoluted around the first element, but I had accepted that as the cost of such type safety.
More recently I toyed around the idea of using a GADT to enable using the actual list literal syntax and constructor for pattern matching:
module List_nonempty = struct
type ('a, 'empty) maybe =
| ( :: ) :
'a * ('a, [< `Nonempty | `Empty ]) maybe
-> ('a, [ `Nonempty ]) maybe
| [] : ('a, [ `Empty ]) maybe
let hd = function
| hd :: _ -> hd
let tl = function
| _ :: tl -> tl
let rec to_list = function
| [ last ] -> Caml.List.[ last ]
| hd :: next :: tl -> Caml.List.cons hd (to_list (next :: tl))
type 'a t = ('a, [ `Nonempty ]) maybe
end
This makes using literal syntax possible, List_nonempty.(hd [0; 1; 2])
has type int
as expected, List_nonempty.([ 0 ] |> tl |> hd)
is a type error as expected, etc. However the type differs entirely from List.t
, while with the previous solution at least the tail was a standard list. This made operating on the rest of the nonempty-list a breeze, and to_list
was very efficient (just prepend the head to the tail). Here because the ::
constructors differ, to_list
allocates an entirely new list, which may be unacceptable performance-wise. I had a mixed feeling about it.
Earlier today however, it struck me that since OCaml has become much better at resolving the correct constructs when it has a full type constraint on an expression, maybe I could get a :: b :: _
to resolve to two different ::
constructors in a single expression. Actually it should work out of the box ā¦ and lo and behold, this completely trivial and much simpler solution entirely does the trick:
module List_nonempty = struct
type 'a t = ( :: ) : 'a * 'a list -> 'a t
let map (hd :: tl) ~f = f hd :: List.map ~f tl
let hd = function
| hd :: _ -> hd
let tl = function
| _ :: tl -> tl
let to_list = function
| hd :: tl -> List.cons hd tl
end
This has all the static typing aforementioned properties, while being essentially transparent syntax-wise:
# let l = List_nonempty.[0; 1; 2];;
val l : int List_nonempty.t = List_nonempty.(::) (0, [1; 2])
# List_nonempty.hd l;;
- : int = 0
# List_nonempty.tl l;;
- : int list = [1; 2] (* the tail is a standard list *)
# List_nonempty.(hd (tl l));;
Error: This expression has type int list
but an expression was expected of type 'a List_nonempty.t
And of_list
is trivial and performant.
No more List.hd |> Option.value_exn
without significantly complexifying the codebase !
Sometimes, I think a feature-full List_nonempty module would be cool in batteries, or as a standalone library.
It should support most BatList operations (and only have tail rec. code).
My chance to plug core_kernel/nonempty_list at master Ā· janestreet/core_kernel Ā· GitHub.
One extra neat trick up its sleeve: it uses the (::)
constructor instead of a tuple, so you can write something like:
let x : _ Nonempty_list.t = [ 1; 2; 3 ]
Ah, so it does exist in Core, thanks!
Regarding the ::
constructor, that was the final iteration of my post too