Alan Wayne
Alan Wayne

Reputation: 5384

How to use bind and map in place of nested matches

F# 6.0.3

I have seen some solutions on Google that are close to what I need; but being a Newbie I can't quite get how to use bind and map to get the solution.

I have many working procedures of the following format:

Example #1:

let saveAllDiagnosis =
                    let savealldiagnosis = match m.Encounter with
                                           | None -> failwith "No encounter found"
                                           | Some e -> match e.EncounterId with
                                                       | None -> failwith "No Encounter id found"
                                                       | Some id ->  m.AllDiagnosisList 
                                                                       |> List.iter ( fun dx -> match dx.Key with
                                                                                                | None -> ()
                                                                                                | Some k -> Async.RunSynchronously (editAllDiagnosisInPreviousEncountersAsync id dx)) 


                savealldiagnosis

Example #2

let saveEncounterDiagnosis = 
                    let savedx = match m.Encounter with 
                                 | None -> failwith "No encounter found"
                                 | Some e -> match e.EncounterId with
                                             | None -> failwith "No Encounter id found"
                                             | Some id -> m.BillingDiagnosisList |> List.iter ( fun dx -> Async.RunSynchronously (saveDxAsync id dx))
                    savedx   

As can be seen, these are nested methods with almost identical behavior--differing only in the async procedure being called and the initializing list. What I would like to do is something along the lines of:

let runProcedures (fn: Model->Async) Model = ????

That is, a single procedue that encapsulates everything except the Async method and it's parameters but manages all the "None"s in a better way.

I hope my intent is clear.

TIA

Upvotes: 0

Views: 194

Answers (3)

Alan Wayne
Alan Wayne

Reputation: 5384

The solutions posted above are very helpful to this newbie. But adding my own two cents worth, I going with this:

let _deleteDxFromEncounterAsync (encounterId:int) (dx:Diagnosis) : Async<unit> = deleteDxFromEncounterAsync encounterId dx.Description
let _deleteDxFromAllPreviousEncountersAsync (encounterId:int) (dx:Diagnosis) : Async<unit> = deleteDxFromAllPreviousEncountersAsync encounterId  dx.Description   
let _saveDxAsync (encounterId:int) (dx:Diagnosis) : Async<unit> = saveDxAsync encounterId dx
let _editAllDiagnosisInPreviousEncountersAsync (encounterId:int) (dx:Diagnosis) : Async<unit> = editAllDiagnosisInPreviousEncountersAsync encounterId dx 

let listchk (dxs:Diagnosis list) : Diagnosis list option =
                match dxs with
                | [] -> None
                | _ -> Some dxs
    
let _save (fn:int -> Diagnosis-> Async<unit>) (dxs:Diagnosis list) : unit = 
 match dxs |> listchk, m.Encounter |> Option.bind (fun v -> v.EncounterId) with
 | Some dxs, Some id -> dxs |> List.iter (fun dx ->   Async.RunSynchronously(fn id dx)) 
 | _,_ -> failwith "Missing Encounter or EncounterId or Empty List"
   

 
m.DeletedBillingDiagnosis |>_save _deleteDxFromEncounterAsync
m.DeletedAllDiagnosis     |>_save _deleteDxFromAllPreviousEncountersAsync
m.BillingDiagnosisList    |>_save _saveDxAsync                                 
m.AllDiagnosisList        |> List.filter (fun dx -> dx.Key.IsSome)    |>_save _editAllDiagnosisInPreviousEncountersAsync   

For speed, in the future, I will probably have the Async functions act on the entire list at one time rather then one item; but for now, this code comes closest to my intent in asking the question. IMPROVEMENTS AND CRITISM IS GLADDLY APPRECIATED! F# is fun!

Thanks to all.

Upvotes: 0

Tomas Petricek
Tomas Petricek

Reputation: 243041

If you are happy with using exceptions, then you do not even need railway-oriented programming (ROP). ROP is useful for more complex validation tasks, but I think exceptions are often perfectly reasonable and easy way of handling errors. In your case, you could define a helper that extracts a value of option<'T> or fails with a given error message:

let orFailWith msg opt = 
  match opt with 
  | Some v -> v
  | None -> failwithf "%s" msg

Using this, you can then rewrite your code as follows:

let saveAllDiagnosis =
  let e = m.Encounter |> orFailWith "No encounter found"
  let id = e.EncounterId |> orFailWith "No Encounter id found"
  for dx in m.AllDiagnosisList do
    dx.Key |> Option.iter (fun k ->
      editAllDiagnosisInPreviousEncountersAsync id dx |> Async.RunSynchronously)

let saveEncounterDiagnosis = 
  let e = m.Encounter |> orFailWith "No encounter found"
  let id = e.EncounterId |> orFailWith "No Encounter id found"
  for dx in m.BillingDiagnosisList do
    saveDxAsync id dx |> Async.RunSynchronously

As I do not know the broader context of this, it is hard to say more - your code is imperative, but that may be perfectly fine if you are following the sandwich pattern.

Upvotes: 3

JL0PD
JL0PD

Reputation: 4488

Using mentioned ROP code can be rewritten as such. Result is used to track error and throw it at the end of pipeline. With current design is possible to avoid exceptions by just logging error instead of throwing at before last line.

type Encounter = { EncounterId : int option }

type Diagnostic = { Key : int option }

type Thing = {
    Encounter : Encounter option
    AllDiagnosisList : Diagnostic list
}

let editAllDiagnosisInPreviousEncountersAsync id diag = async { return () }

module Result =
    let ofOption err opt =
        match opt with
        | Some v -> Ok v
        | None -> Error err

    let join res =
        match res with
        | Error v
        | Ok v -> v

let saveAllDiagnosis m =
    m.Encounter
    |> Result.ofOption "No encounter found" // get value from option or log error
    |> Result.map (fun e -> e.EncounterId)
    |> Result.bind (Result.ofOption "No Encounter id found") // get EncounterId or log error
    |> Result.map (fun id -> (
        m.AllDiagnosisList
        |> Seq.where (fun dx -> dx.Key.IsSome)
        |> Seq.iter (fun dx -> Async.RunSynchronously (editAllDiagnosisInPreviousEncountersAsync id dx))
    ))
    |> Result.mapError failwith // throw error
    |> Result.join // Convert Result<unit, unit> into unit

Upvotes: 1

Related Questions