F# 计算表达式:可以使用一个来简化这段代码吗?

F# computation expressions: Can one be used to simplify this code?

我最近开始使用计算表达式来简化我的代码。到目前为止,对我唯一有用的是 MaybeBuilder,定义如下:

type internal MaybeBuilder() =

    member this.Bind(x, f) = 
        match x with
        | None -> None
        | Some a -> f a

    member this.Return(x) = 
        Some x

    member this.ReturnFrom(x) = x

但我想探索其他用途。一种可能是我目前面临的情况。我有一些定义对称矩阵的供应商提供的数据。为了保存 space,只给出了矩阵的三角形部分,因为另一边只是转置。所以如果我在 csv 中看到一行

abc, def, 123

这意味着行 abc 和列 def 的值为 123。但是我不会看到诸如

的行

def, abc, 123

因为由于矩阵的对称性,此信息已经给出。

我已将所有这些数据加载到 Map<string,Map<string,float>> 中,并且我有一个函数可以为我获取任何条目的值,如下所示:

let myLookupFunction (m:Map<string,Map<string,float>>) s1 s2 =
    let try1 =
        match m.TryFind s1 with
        |Some subMap -> subMap.TryFind s2
        |_ -> None

    match try1 with
    |Some f -> f
    |_ ->
        let try2 =
            match m.TryFind s2 with
            |Some subMap -> subMap.TryFind s1
            |_ -> None
        match try2 with
        |Some f -> f
        |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

既然我了解了计算表达式,我怀疑可以隐藏匹配语句。 我可以像这样使用 MaybeBuilder 稍微清理一下

let myFunction2  (m:Map<string,Map<string,float>>) s1 s2 =
    let maybe = new MaybeBuilder()
    let try1 = maybe{
        let! subMap = m.TryFind s1
        return! subMap.TryFind s2
    }

    match try1 with
    |Some f -> f
    |_ -> 
        let try2 = maybe{
            let! subMap = m.TryFind s2
            return! subMap.TryFind s1
        }
        match try2 with
        |Some f -> f
        |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

这样一来,我的匹配语句从 4 个减少到 2 个。是否有一种(非人为的)方法可以通过使用计算表达式进一步清理它?

首先,每次需要时创建一个新的MaybeBuilder有点浪费。你应该这样做一次,最好是紧挨着 MaybeBuilder 本身的定义,然后在任何地方都使用同一个实例。这就是大多数计算构建器的工作方式。

其次: 如果您将 "try" 逻辑定义为一个函数并重用它,您可以减少混乱的数量:

let myFunction2  (m:Map<string,Map<string,float>>) s1 s2 =
    let try' (x1, x2) = maybe{
        let! subMap = m.TryFind x1
        return! subMap.TryFind x2
    }

    match try' (s1, s2) with
    |Some f -> f
    |_ ->         
        match try' (s2, s1) with
        |Some f -> f
        |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

Third,注意你使用的模式:试试这个,如果不行试试那个,如果不行再试试另一个,等等。模式可以抽象为函数(这就是全部演出!),让我们开始吧:

let orElse m f = match m with
   | Some x -> Some x
   | None -> f()

let myFunction2  (m:Map<string,Map<string,float>>) s1 s2 =
    let try' (x1, x2) = maybe{
        let! subMap = m.TryFind x1
        return! subMap.TryFind x2
    }

    let result = 
        try' (s1, s2)
        |> orElse (fun() -> try' (s2, s1))

    match result with
    |Some f -> f
    |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

最后,我认为你的做法是错误的。您真正想要的似乎是 具有 two-part 对称密钥 的字典。那么为什么不这样做呢?

module MyMatrix =
    type MyKey = private MyKey of string * string
    type MyMatrix = Map<MyKey, float>

    let mkMyKey s1 s2 = if s1 < s2 then MyKey (s1, s2) else MyKey (s2, s1)


let myFunction2 (m:MyMatrix.MyMatrix) s1 s2 =
    match m.TryFind (MyMatrix.mkMyKey s1 s2) with
    | Some f -> f
    | None -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

在这里,MyKey 是一种封装一对字符串的类型,但保证这些字符串是 "in order" - 即第一个在字典序上比第二个 "less"。为了保证这一点,我将类型的构造函数设为私有,而是公开了一个函数 mkMyKey 来正确构造密钥(有时称为 "smart constructor")。

现在您可以自由使用MyKey来构建和查找地图。如果你输入(a, b, 42),你会得到(a, b, 42)(b, a, 42)

旁白:我在您的代码中看到的一般错误是未能使用抽象。您不必在最低级别处理每条数据。该语言允许您定义 higher-level 概念,然后根据它们进行编程。使用那个能力。

我知道这可能只是为了在此处提出问题而进行的简化 - 但是当找到 none 个密钥时您实际想要做什么以及您希望第一个密钥多久出现一次查找会失败吗?

在 F# 中避免异常是有充分理由的 - 它们速度较慢(我不知道到底有多慢,这可能取决于您的用例)并且它们应该用于 "exceptional circumstances",但是语言确实对它们有很好的支持。

使用异常,你可以把它写成一个非常可读的three-liner:

let myLookupFunction (m:Map<string,Map<string,float>>) s1 s2 =
  try m.[s1].[s2] with _ -> 
  try m.[s2].[s1] with _ ->
    failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

就是说,我完全同意 Fyodor 的观点,即定义自己的数据结构来保存数据比使用地图的地图(可能带有切换键)更有意义。