F# 计算表达式:是否可以使用一个表达式来简化此代码?



我最近开始使用计算表达式来简化我的代码。到目前为止,对我来说唯一有用的是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

但我想探索其他用途。一种可能性是在我目前面临的情况中。我有一些由定义对称矩阵的供应商提供的数据。为了节省空间,只给出了矩阵的三角形部分,因为另一侧只是转置。因此,如果我在 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)

第三,注意你正在使用的模式:试试这个,如果不试试那个,如果不试试另一个,等等。 模式可以抽象为函数(这就是整个演出!),所以让我们这样做:

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)

最后,我认为你走错了路。你真正想要的是一本由两部分组成的对称键的字典。那么为什么不这样做呢?

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是一种封装一对字符串的类型,但保证这些字符串是"有序的"——即第一个字符串在字典上比第二个字符串"少"。为了保证这一点,我将类型的构造函数设为私有,而是公开了一个正确构造键的函数mkMyKey(有时称为"智能构造函数")。

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

撇开一些:我在你的代码中看到的一般错误是未能使用抽象。您不必在最低级别处理每条数据。该语言允许您定义更高层次的概念,然后根据它们进行编程。使用这种能力。

我知道这可能只是为了在这里提出问题而简化 - 但是当找不到任何键时,您实际上想做什么,您预计第一次查找失败的频率是多少?

有充分的理由避免 F# 中的异常 - 它们更慢(我不知道确切多少,这可能取决于您的用例),并且它们应该在"特殊情况"中使用,但该语言确实对它们有很好的支持。

使用异常,您可以将其编写为非常可读的三行:

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 的观点,即定义自己的数据结构来保存数据而不是使用地图地图(可能切换键)是很有意义的。

相关内容

  • 没有找到相关文章