Skip to content

Instantly share code, notes, and snippets.

@xiaoyvr
Last active January 22, 2023 15:34
Show Gist options
  • Save xiaoyvr/cc5e3f0359fc615dba81115e399c144d to your computer and use it in GitHub Desktop.
Save xiaoyvr/cc5e3f0359fc615dba81115e399c144d to your computer and use it in GitHub Desktop.
// step0: quick intro about paket, fake, |> operator and unit
// ------------------ begin ----------------
(*
The problem: lots of boilerplate code to make sure the exit code is being handled properly in our build script.
when we have more steps, the code will smell worse.
*)
let DotNetExec cmd =
DotNet.exec id cmd ""
Target.create "TestExitCode1" (fun _ ->
let result1 = DotNetExec "build"
if result1.ExitCode = 0 then
failwith "Build failed. "
let result2 = DotNetExec "test"
if result1.ExitCode = 0 then
failwith "Test failed. "
)
(*
Lets chain it together to reduce the boilerplate code,
since we don't have a function to take ProcessResult as the last parameter, we created another version
of our function DotNetExec2 to take the extra parameter.
*)
let failWithExitCode (p:ProcessResult) =
if p.ExitCode <> 0 then failwith "Process execution failed. "
let DotNetExec2 str (pr:ProcessResult) =
if pr.ExitCode = 0 then
pr
else
DotNetExec str
Target.create "TestExitCode0" (fun _ ->
DotNetExec "build" |> DotNetExec2 "test" |> failWithExitCode
)
(*
code smell: 2 method has similar names, can we make it more clear?
let's use high order function to alter the original function instead of create another one
*)
let addExtraParam (f: string -> ProcessResult) =
fun str (p:ProcessResult) ->
if p.ExitCode <> 0 then
p
else
f str
Target.create "TestExitCode1" (fun _ ->
DotNetExec "build" |> addExtraParam DotNetExec "test" |> failWithExitCode
)
(*
code smell: addExtraParam bound with string and ProcessResult, can we make it more generic?
so that other processes take different parameter or have a different return structure can also use it.
let's make it more generic with type parameter and SRTP (Statically Resolved Type Parameters)
*)
let inline addExtraParamGeneric<'T, ^PR when ^PR: (member ExitCode: int)> (f: 'T -> ^PR) =
fun t (p:^PR) ->
let exitCode = (^PR :(member ExitCode: int) p)
if exitCode <> 0 then
p
else
f t
Target.create "TestExitCode2" (fun _ ->
DotNetExec "build" |> addExtraParamGeneric DotNetExec "test" |> failWithExitCode
)
(*
let's use operator overload to make it more compact
*)
let (!^+) = addExtraParamGeneric
Target.create "TestExitCode21" (fun _ ->
DotNetExec "build" |> !^+ DotNetExec "test" |> failWithExitCode
)
(*
only use the original function with an generic operator overload, pretty good, right?
now, the problem is, we lost our exit reason while we chain the method together.
but we still want to report which step it failed. let's use a construct to capture it.
*)
type Exitable =
| Exit of string
| Continue
let ExitableDotNetExec cmd =
let r = DotNet.exec id cmd ""
if r.ExitCode = 0 then
Continue
else
Exit($"Exec {cmd} failed.")
let failExitable exitable =
match exitable with
| Exit s -> failwith s |> ignore
| _ -> ()
let liftAsExitable f =
fun s exitable ->
match exitable with
| Exit s -> Exit(s)
| _ -> f s
Target.create "TestExitCode3" (fun _ ->
ExitableDotNetExec "build" |> liftAsExitable ExitableDotNetExec "test" |> failExitable
)
(*
You can still use operator overload to make the code more compact.
now the problem is, liftAsExitable bound with the signature of ('a -> Exitable), what if there is another function take
2 or more parameters? can we make the liftAsExitable more generic reusable, so that other functions, with more parameters,
returns an Exitable, can also use it?
let's introduce a more generic bind method, take an exitable and an (Unit -> Exitable)
*)
type Exitable with
static member bind (e, f) =
match e with
| Exit s -> Exit(s)
| Continue -> f ()
(*
Now the bind function can take a function takes a Unit, which means takes nothing, and returns a Exitable. now let's
convert our function invocation to it. let's call it ```later```.
*)
let later1 f p1 = fun() -> f p1
let (!/>) = later1
//let ExitableDotNetExecLater = later1 ExitableDotNetExec
Target.create "TestExitCode31" (fun _ ->
Exitable.bind (Exitable.bind (Continue, !/> ExitableDotNetExec "build"), !/> ExitableDotNetExec "test") |> failExitable
)
(*
Now the bind function can take a function takes a Unit, which means takes nothing, and returns a Exitable. All
function invocation can be convert to it.
But it is hard to read. Again, let's make it more readable by operator overload.
*)
let (>==>) m f = Exitable.bind(m, f)
Target.create "TestExitCode32" (fun _ ->
Continue >==> !/> ExitableDotNetExec "build" >==> !/> ExitableDotNetExec "test" |> failExitable
// can also be
// !/> ExitableDotNetExec "build" () >==> !/> ExitableDotNetExec "test" |> failExitable
)
(*
operator >==> now bound with Exitable and (Unit -> Exitable). since operator is more valuable (we only have so many).
we should make it more generic, so that other similar types have a bind method can also use this operator.
let's use SRTP, again
*)
let inline (>>=)<'v, ^M when ^M: (static member bind: ^M -> ('v -> ^M) -> ^M)> (m:^M) (f: 'v -> ^M) : ^M =
(^M :(static member bind: ^M -> ('v -> ^M) -> ^M) m,f )
Target.create "TestExitCode33" (fun _ ->
Continue >>= !/> ExitableDotNetExec "build" >>= !/> ExitableDotNetExec "test" |> failExitable
// can also be
// !/> ExitableDotNetExec "build" () >>= !/> ExitableDotNetExec "test" |> failExitable
)
(*
Now we decoupled (>>=) with Exitable. it is more generic. it should be able to chain anything together, as long as
"the thing" has a static bind method. use your imagination.
Wait, is this a ... monad ???
lets verify with 3 laws (<==> means is the same with)
1. left identity: ```return x >>= f``` <==> ```f x```
2. right identity: ```m >>= return``` <==> ```m```
3. associativity: ```(m >>= f) >>= g``` <==> ```m >>= (fun x -> f x >>= g)```
in our case, the type constructor (m) is Exitable , the underlying type (x) is Unit
lets define the return as:
let return () = Continue
law 1:
return () >>= f
<==> bind (return (), f)
<==> bind (Continue, f)
<==> f ()
law 1 passed!
law 2:
assume m is Continue:
m >>= return
<==> bind (m, return)
<==> bind (Continue, return)
<==> return ()
<==> Continue
<==> m
assume m is Exit(msg):
bind (m, return)
<==> bind (Exit(msg), return)
<==> Exit(msg)
<==> m
law 2 also passed!
law 3:
assume m is Continue
left side: (m >>= f) >>= g
<==> (bind (bind Continue, f), g)
<==> (bind f(), g)
right side: m >>= (fun () -> f () >>= g)
<==> (bind Continue, (fun () -> f () >>= g))
<==> (bind Continue, (fun () -> (bind f (), g)))
<==> (fun () -> (bind f (), g)) ()
<==> (bind f(), g)
left side <==> right side
assume m is Exit(msg)
left side: (m >>= f) >>= g
<==> (bind (bind Exit(msg), f), g)
<==> Exit(msg)
right side: m >>= (fun x -> f x >>= g)
<==> (bind Exit(msg), (fun () -> f () >>= g))
<==> Exit(msg)
left side <==> right side
3 laws all passed! We really got a monad, people!!!
Monad has another syntax construct in F#, computation expression.
Let's try to use computation expression to finish our exercise.
*)
type ExitableBuilder() =
member this.Bind(m, f) =
match m with
| Exit s -> Exit(s)
| Continue -> f ()
member this.Zero() =
Continue
let exitable = ExitableBuilder()
Target.create "TestExitCode4" (fun _ ->
exitable {
do! ExitableDotNetExec "build"
do! ExitableDotNetExec "test"
} |> failExitable
)
(*
closing:
* pipe expressions instead of write sequence of instructions
* use high order function to alter the original function to make the pipe
* use operator overload to make the pipe looks more compact
* make operator as generic as possible, so that it can be reused
* lift the original type to Union to pass information through the pipe
* define bind (>>=) to express the computation through the pipe
* can also choose to use computation expression instead of pipe to make it more readable
How deep you can go to solve a really trival problem?
*)
// ------------------the end ----------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment