3

This seems to work but it seems clunky. Is there a better way to code this?

// Hunting for the best index to use for a data compare
let getIndex connDB strTable =
    match getIndexByPrimaryKey connDB strTable with
    | Some(name) -> Some(name)  
    | None ->
    match getIndexByCluster connDB strTable with
    | Some(name) -> Some(name)
    | None -> 
    match getIndexByFirstColumns connDB strTable with
    | Some(name) -> Some(name)
    | None -> 
    match getIndexByOnlyUnique connDB strTable with
    | Some(name) -> Some(name)
    | None -> 
    match getAnyUniqueIndex connDB strTable with
    | Some(name) -> Some(name)
    | None -> None
dbc
  • 104,963
  • 20
  • 228
  • 340
JayR
  • 121
  • 3
  • 3
    I bet you can use Option.bind in a pipeline: http://msdn.microsoft.com/en-us/library/ee353901.aspx – Brian Sep 26 '12 at 21:36
  • 3
    if should be have inverted meaning - ask the next handler if current fails – desco Sep 26 '12 at 21:37

3 Answers3

8

Probably the most concise version is to use List.tryPick:

let getIndex connDB strTable =
  [ getIndexByPrimaryKey;
    getIndexByCluster;
    getIndexByFirstColumns;
    getIndexByOnlyUnique;
    getAnyUniqueIndex; ]
  |> List.tryPick (fun fn -> fn connDB strTable)

UPDATE:

The technique can be easily extended to use closures hence it works for functions with different arguments (with a lot of funs as well :-)):

let getIndex connDB strTable =
  [ fun () -> getIndexByPrimaryKey connDB strTable;
    fun () -> getIndexByCluster connDB strTable;
    fun () -> getIndexByFirstColumns connDB strTable;
    fun () -> getIndexByOnlyUnique connDB strTable;
    fun () -> getAnyUniqueIndex connDB strTable; ]
  |> List.tryPick (fun fn -> fn())
pad
  • 41,040
  • 7
  • 92
  • 166
  • I tested this and it does appear to be lazy. In this example a break point on t3 is never reached. let t1 () = None let t2 () = Some(2+2) let t3 () = Some(3+3) let test = [t1; t2; t3] |> List.tryPick (fun fn -> fn ()) let t3 () = Some(3+3) let test = [t1; t2; t3] |> List.tryPick (fun fn -> fn ()) – JayR Sep 26 '12 at 22:02
  • Yes, it doesn't have to go through the whole list. See `List.tryPick` implementation h[ere](https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/list.fs#L303). – pad Sep 26 '12 at 22:03
  • This is definitely the shortest, given that the same params are passed to each function. My answer addresses the general problem. – Daniel Sep 26 '12 at 22:06
  • @Daniel: I addressed the general case using closures in my answer. Of course, there is no need to do so in this example. – pad Sep 26 '12 at 22:18
3

I think the cleanest option is to define the getIndexByXYZ operations as active patterns rather than as functions. Then you can write the following pattern matching:

let getIndex connDB strTable = 
    match connDB, strTable with
    | IndexByPrimaryKey name 
    | IndexByCluster name 
    | IndexByFirstColumns name 
    | IndexByOnlyUnique name 
    | AnyUniqueIndex name -> Some(name)   

If you still want to use the functions in other parts of your program in a context where you're not pattern matching on them, then you can define active patterns as simple wrappers:

let (|IndexByPrimaryKey|_|) (connDB, strTable) =
  getIndexByPrimaryKey connDB strTable

Sadly, there is no way to turn the functions into active patterns "automatically" but I think it is well worth the additional effort if you need to express some business logic and want it to be readable.

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • It's not "automatic," but I included in my answer a way to call a function within an active pattern. – Daniel Sep 27 '12 at 14:46
1

I would write an orElse function. Then you could do this:

let orElse f = function
  | None -> f()
  | Some _ as x -> x

let getIndex connDB strTable =
  getIndexByPrimaryKey connDB strTable
  |> orElse (fun () -> getIndexByCluster connDB strTable)
  |> orElse (fun () -> getIndexByFirstColumns connDB strTable)
  |> orElse (fun () -> getIndexByOnlyUnique connDB strTable)
  |> orElse (fun () -> getAnyUniqueIndex connDB strTable)

Or, if you prefer "workflow" syntax (and need this often), this:

module OrElse =

  let bind f = function
    | None -> f()
    | Some x -> Some x

  let combine m1 m2 =
    m1 |> bind (fun () -> m2)

  type OrElseBuilder() =
    member x.Zero() = None
    member x.Return(v) = Some v
    member x.Bind(m, f) = bind f m
    member x.ReturnFrom(m) = m
    member x.Combine(m1, m2) = combine m1 m2
    member x.Delay(f) = f()

  let orElse = OrElseBuilder()

will let you state it even more consicely:

open OrElse

orElse {
  return! getIndexByPrimaryKey connDB strTable
  return! getIndexByCluster connDB strTable
  return! getIndexByFirstColumns connDB strTable
  return! getIndexByOnlyUnique connDB strTable
  return! getAnyUniqueIndex connDB strTable
}

Since you're passing the same args to every function, pad's solution is likely as concise as possible. These solutions address the general problem of replacing nested match x with Some _ as v -> v | None -> ....

EDIT

Extending Tomas' idea, you could use a general-purpose active pattern for "patternizing" functions:

let (|FN|_|) f x = f x

Then do:

match connDB, strTable with
| FN getIndexByPrimaryKey name -> Some name
| FN getIndexByCluster name -> Some name
| FN getIndexByFirstColumns name -> Some name
| FN getIndexByOnlyUnique name -> Some name
| args -> getAnyUniqueIndex args

This requires one slight change to your functions: the args must be in tupled form.

Daniel
  • 47,404
  • 11
  • 101
  • 179
  • 2
    I think that defining ad-hoc computation expressions that do not follow any standard pattern is just going to make the code very confusing. This is really a mis-use of the feature and it does not give you any advantage over creating a list of functions as in @pad's answer. – Tomas Petricek Sep 26 '12 at 22:32
  • Just curious, how do you define "standard" here? Given that defining computation expressions isn't limited by the language, how would one determine what is acceptable? – Daniel Sep 26 '12 at 22:36
  • A more principled way to look at it is that your `orElse` computation builder does not obey any of the usual monad laws, so it is probably wrong use. I'm not _too_ sensitive to this, but when your `bind` has a wrong type, it is a bit scary :-). Now, I think there is a way to express this sort of thing using a computation expression, but 1) using a different, less ad-hoc one and 2) it is not really helpful in this case anyway. – Tomas Petricek Sep 26 '12 at 22:37
  • I tried to define the "standard" here: http://www.cl.cam.ac.uk/~tp322/papers/notations.pdf :-). Of course, I'm not saying that _my list_ is the _right list_. But it should give you an idea about what I consider a convincing definition. If you think that paper is missing some useful reusable structure, then feel free to document it and I'm happy to add it there. (BTW: I think there is already one that would fit this purpose, but is still usable with other types.) – Tomas Petricek Sep 26 '12 at 22:42
  • I won't argue your second point. I'd like to see your "less ad-hoc" approach. – Daniel Sep 26 '12 at 22:43
  • One concrete objection to the example here is that it is not uniform. Why do I need to write `do!` for the first few cases and `return!` for the last one? That does not have any sense. But if you write just `do!` then it won't work... – Tomas Petricek Sep 26 '12 at 22:46
  • Re "less ad-hoc" approach: I think this computation type would probably match the semigroup pattern (see Section 4). – Tomas Petricek Sep 26 '12 at 22:48
  • 1
    I like your `orElse` _function_; I've often wished for something just like that in the Option module. – ildjarn Sep 26 '12 at 22:53
  • BTW: I like the `orElse` _function_ too :-). – Tomas Petricek Sep 26 '12 at 23:04
  • I'd love to see this formulated using the semigroup pattern. I'm having a hard time seeing it. – Daniel Sep 27 '12 at 02:32
  • I think if you define `Combine(a,b)` to return `a` if it is `Some` and `b` otherwise and `ReturnFrom(a)=a` then that would work and should be associative. Then you would write something like your sample, but with `return!` for everything. – Tomas Petricek Sep 28 '12 at 13:00
  • Thanks! Good suggestion (code updated). `Bind` still has an unorthodox definition. I'm not sure if that's a problem...seems to work as expected. I'm a little surprised this isn't widely considered a desirable solution. A computation expression is a great way to express a series of contingencies. – Daniel Sep 28 '12 at 14:32
  • I'm starting to think I'm a computation expression "abuser." Brian had some choice words about [my solution](http://stackoverflow.com/questions/11976652/how-can-i-refactor-out-the-required-else-clause#comment15990288_11977444) to a similar problem. Granted, my answer was more a curiosity and was certainly overkill in that case. But, whereas I think of CEs as little more than syntactic sugar for combinators, many programmers have a limited, fixed view of when and how they should be used. I understand monads have a definite theoretical basis, but I'm unclear on the effects of such "abuse." – Daniel Sep 28 '12 at 14:40