Ok, I better understand what you want to do with the Staged
module but I don’t find this very natural.
Maybe could I try an hypothesis: the pipe |>
is a primitive, but the reverse composition %>
and the composition %
combinators are not. Here their definitions:
let (%>) f g x = g (f x)
let (%) f g x = f (g x)
external (@@) : ('a -> 'b) -> 'a -> 'b = "%apply"
external (|>) : 'a -> ('a -> 'b) -> 'b = "%revapply"
According to batteries source code, they were introduced into the language for version 4 in July 2012.
Now, consider this little benchmark (I also added Staged
in the test):
open! Core
open Core_bench
open Base
open Re2
let (%>) f g x = g (f x)
let (%) f g x = f (g x)
(*let (|>) x f = f x*)
let f : int -> string -> int = fun x ->
ignore (Regex.(matches (of_string "[0-9]+") "foo"));
fun y -> x + String.length y
let h : int -> (string -> int) Staged.t = fun x ->
ignore (Regex.(matches (of_string "[0-9]+") "foo"));
Staged.stage (fun y -> x + String.length y)
let g : int -> int -> int = fun x y -> x * y
(* (fun f g -> fun x -> x |> g |> f) (g 2) (f 1) *)
let pipeline_composed : string -> int =
g 2 % f 1
let pipeline_revcomposed : string -> int =
f 1 %> g 2
let pipeline_with_pipe : string -> int =
fun x -> x |> f 1 |> g 2
(* a quasi-inline version of pipeline_composed *)
let pipeline_with_pipe_bis : string -> int =
let f_applied = f 1 in
let g_applied = g 2 in
fun x -> x |> f_applied |> g_applied
let pipeline_composed_unstage : string -> int =
g 2 % Staged.unstage (h 1)
let pipeline_revcomposed_unstage : string -> int =
Staged.unstage (h 1) %> g 2
let pipeline_with_pipe_unstage : string -> int =
let h_unstage = Staged.unstage (h 1) in
(fun x -> x |> h_unstage |> g 2)
let () =
let composed = Test.create
~name:"composed"
(fun () -> pipeline_composed "foo")
in
let revcomposed = Test.create
~name:"revcomposed"
(fun () -> pipeline_revcomposed "foo")
in
let with_pipe = Test.create
~name:"with pipe"
(fun () -> pipeline_with_pipe "foo")
in
let with_pipe_bis = Test.create
~name:"with pipe bis"
(fun () -> pipeline_with_pipe_bis"foo")
in
let composed_unstage = Test.create
~name:"composed unstage"
(fun () -> pipeline_composed_unstage "foo")
in
let revcomposed_unstage = Test.create
~name:"revcomposed unstage"
(fun () -> pipeline_revcomposed_unstage "foo")
in
let with_pipe_unstage = Test.create
~name:"with pipe unstage"
(fun () -> pipeline_with_pipe_unstage "foo")
in
Bench.make_command [
composed;
revcomposed;
with_pipe;
with_pipe_bis;
composed_unstage;
revcomposed_unstage;
with_pipe_unstage;
]
|> Command.run
and its result:
Estimated testing time 1.75s (7 benchmarks x 250ms). Change using -quota SECS.
Name Time/Run mWd/Run Percentage
--------------------- ------------- --------- ------------
composed 8.44ns 0.08%
revcomposed 8.44ns 0.08%
with pipe 10_455.23ns 7.00w 100.00%
with pipe bis 4.75ns 0.05%
composed unstage 8.44ns 0.08%
revcomposed unstage 8.44ns 0.08%
with pipe unstage 6.52ns 0.06%
the naive use of the pipe is clearly inneficient, so I remove it from the bench.
Estimated testing time 1.5s (6 benchmarks x 250ms). Change using -quota SECS.
Name Time/Run Percentage
--------------------- ---------- ------------
composed 7.91ns 99.27%
revcomposed 7.97ns 100.00%
with pipe bis 4.48ns 56.29%
composed unstage 7.93ns 99.59%
revcomposed unstage 7.91ns 99.25%
with pipe unstage 6.37ns 79.97%
Here the pipe is clearly more efficient, even with the staged version. But now, I redefine the pipe |>
to not use the primitive version:
let (|>) x f = f x
and here the result:
Estimated testing time 1.5s (6 benchmarks x 250ms). Change using -quota SECS.
Name Time/Run mWd/Run Percentage
--------------------- ---------- --------- ------------
composed 8.43ns 83.26%
revcomposed 8.43ns 83.23%
with pipe bis 8.45ns 83.43%
composed unstage 9.03ns 89.15%
revcomposed unstage 8.96ns 88.45%
with pipe unstage 10.13ns 5.00w 100.00%
the gain form the use of pipe |>
disappear.
Maybe if the composition combinators were primitives they would be as performant as the pipe.