F # when mixing DU and other values

What's the most efficient way to express the following code?

match cond.EvalBool() with
| true ->                
    match body.Eval() with
    | :? ControlFlowModifier as e ->
        match e with
        | Break(scope) -> e :> obj //Break is a DU element of ControlFlowModifier
        | _ -> next() //other members of CFM should call next()
    | _ -> next() //all other values should call next()
| false -> null

      

cond.EvalBool returns a boolean result in which false should return null and true should either run the entire block again (its wrapped in func called next) or if a special value break is found then the loop should exit and return break.

Is there a way to compress this block of code into something smaller?

+2


a source to share


6 answers


I think the code you wrote is fine. Here's the alternative that I prefer:



let isBreak = function | Break(_) -> true | _ -> false
if cond.EvalBool() then
  match body.Eval() with
  | :? ControlFlowModifier as e when isBreak e -> e :> obj
  | _ -> next()
else
  null

      

+4


a source


I want to point out that there is a subtype hierarchy for the result type Eval

, and if it is also DU instead, you can do something like

match body.Eval() with
| ControlFlowModifier(Break e) -> box e
| _ -> next()

      



Hooray for nested templates.

+3


a source


I am not too fond of matching booleans instead of using if-else

. What about

let isBreak = function Break _ -> true | _ -> false
...

if cond.EvalBool() then
    match body.Eval() with
    | :? ControlFlowModifier as e when isBreak e -> box e
    | _ -> next()
else null

      

Or if you think a special function isBreak

is unnecessary (I would understand that), try creating a more general function: C # as

operator

let tryCast<'T> (o : obj) =
    match o with
    | :? 'T as x -> Some x
    | _ -> None
...

if cond.EvalBool() then
    match body.Eval() |> tryCast with
    | Some (Break _ as e) -> box e //Break is a DU element of ControlFlowModifier
    | _ -> next() //all other values should call next()
else null

      

+2


a source


I ended up creating an active template for this. Similar logic exists elsewhere, so I can make it reusable

let rec next() : obj =
if cond.EvalBool() then 
    match body.Eval() with
    | IsBreak(res) -> res
    | _ -> step.Eval() |> ignore ; next()
else null

      

Looks decent?

+1


a source


To flatten nested constructors match

, you will need to use nested templates. This is best for discriminatory unions (as Brian pointed out, and I agree that developing F # code to use mostly discriminatory unions is the best you can do).

Otherwise, you will need some active templates if you want to write code succinctly with match

(ssp posted one example that shows active templates specifically for your problem). However, you can do this using the following two active reuse patterns:

let (|TryCast|_|) a : 'res option =    
  match (box a) with 
  | :? 'res as r -> Some(r)
  | _ -> None

let (|Value|) (l:Lazy<_>) = l.Value  

      

The first one is similar :?

, but it allows you to insert other templates according to this value (which is not possible with use as

). The second is evaluating a lazy value (I suppose both can be declared in F # libraries as they are quite useful). Now you can write:

match lazy cond.EvalBool(), lazy body.Eval() with
| Value(true), Value(TryCast((Break(scope) : ControlFlowModifier)) as e) -> 
    e :> obj //Break is a DU element of ControlFlowModifier  
| Value(true), _ -> 
    next() //all other values should call next()  
| _, _ -> null 

      

EDIT: As Roger pointed out in a comment, this version of the code might not be very readable. I think the best option would be to use only TryCast

and a little formatting of the source code (although this is not really standard indentation, this is correct, and the F # compiler does a great job):

match cond.EvalBool() with 
| false -> null 
| true ->                 
match body.Eval() with 
| TryCast(Break(scope) as e) -> e :> obj
| _ -> next()

      

This is probably the most readable parameter based on pattern matching, but you can also use if

instad of the former match

as in the kvb version and combine it with TryCast

(it really depends on personal preference):

if cond.EvalBool() then
  match body.Eval() with 
  | TryCast(Break(scope) as e) -> e :> obj
  | _ -> next()
else null

      

In any case, I believe it TryCast

makes the code more readable since you avoid one nesting (which is required by others because of :? .. as ..

).

+1


a source


If you mean "most efficient way" as the shortest code, I'll also vote for the AP:

let (|CondEval|_|) (c,_) = if c.EvalBool() then Some true else None
let (|BodyEval|_|) (_,b) =
  match b.Eval() with
  | ControlFlowModifier as e -> Some e
  | _ -> None

match cond,body with
| CondEval _ & BodyEval e -> e :> obj
| true -> next()
| false -> null

      

0


a source







All Articles