如何重构这个 Haskell 函数链代码?
how to refactor this Haskell chain of functions code?
我有一些软件设计经验,现在正在学习Haskell。在许多现实世界的软件开发中,人们面临着如下所示的情况:
假设,我有这个代码
f1 a b c d = e
where
e1 = f2 b c (f3 a)
e2 = f4 d
e = e1 + e2
f2 b c d = n + c + d
where
n = f5 b
f5 n = n*n
f3 a = a * 2
f4 a = a + 3
现在,如果我想更改 f5 使其接受另一个参数,我将不得不更改一直到 f1 的所有函数链。
它可以如下所示完成。注意添加的参数 x.
f1 a b c d x = e -- f1 needs to be changed
where
e1 = f2 b c (f3 a) x
e2 = f4 d
e = e1 + e2
f2 b c d x = n + c + d -- f2 needs to be changed
where
n = f5 b x
f5 n x = n*n -- f5 changed (**bang**)
f3 a = a * 2
f4 a = a + 3
这是做这类事情的正常 Haskell 方式还是有更好的(更多 Haskell-ish)方式?
我知道 API 中的这种更改会干扰客户端代码,但如何将影响保持在最低限度,是否有任何 Hasekll 方法?
在更一般的层面上:Haskell 在这种情况下的表现如何(特别是考虑到它的 不可变状态 特性)?在这方面它能为开发人员提供什么?还是 Haskell 本身在这方面没有任何作用,这只是一个困难的软件工程问题(没有 未来证明 这样的问题)我们要跟上吗?
我很抱歉在一个问题中问了多个问题 post,但我无能为力,因为这些问题彼此相关。另外,我找不到类似的问题,抱歉,如果我可能错过了。
正如 bhelkir 在评论中建议的那样,您可以做的一件事是将这样的参数捆绑到一个参数对象中。如果您向该对象添加一个新参数,您仍然需要更改调用 f1
的客户端代码,并更改该新参数的直接使用者(此处为 f5
),但这都是不可避免的:有人必须在某个时候提供 x
,而您需要它才能成为客户;而且你必须以某种方式消耗 x
否则你为什么要添加它?
但是,您可以避免更改像 f1
和 f2
这样的中间函数,因为它们可以忽略它们不关心的新字段。您可以通过使用 ((->) t)
的 Applicative
实例(通常称为 Reader
)来传递此对象,而不是手动执行,从而获得一些乐趣。这是一种写法:
module Test where
import Control.Applicative
data Settings = Settings {getA :: Int,
getB :: Int,
getC :: Int,
getD :: Int}
f1 :: Settings -> Int
f1 = liftA2 (+) f2 f4
-- f1 = do
-- e1 <- f2
-- e2 <- f4
-- return $ e1 + e2
f2 :: Settings -> Int
-- probably something clever with liftA3 and (+) is possible here too
f2 s = f5 s + getC s + f3 s
f3 :: Settings -> Int
f3 = liftA (* 2) getA
f4 :: Settings -> Int
f4 = liftA (+ 3) getD
f5 :: Settings -> Int
-- f5 = liftA (join (*)) getB -- perhaps a bit opaque
f5 = liftA square getB
where square b = b * b
现在,这有优点也有缺点:f1
中的逻辑(即,知道您需要用 a
调用 f3
)已经转移到 f3
本身,这将发生在任何最初读取参数并在将其传递给某个子函数之前对其进行处理的函数。这可能比原来的更清楚,或者它可能掩盖了 f1
背后的意图,具体取决于您的问题域。您始终可以更明确地编写一个函数,例如,让它修改传入的 Settings
对象以在传递它之前更改其 a
字段,就像我以 f2
为例.更一般地说,您可以以最方便的方式编写任何函数:do-notation、applicative functions 或对传入的记录对象进行普通的旧模式匹配。
但最大的优点是添加新参数非常容易:您只需向 Settings
记录添加一个字段,然后在需要它的函数中读取它:
module Test where
import Control.Applicative
data Settings = Settings {getA :: Int,
getB :: Int,
getC :: Int,
getD :: Int,
getX :: Int}
f1 :: Settings -> Int
f1 = liftA2 (+) f2 f4
-- f1 = do
-- e1 <- f2
-- e2 <- f4
-- return $ e1 + e2
f2 :: Settings -> Int
-- probably something clever with liftA3 and (+) is possible here too
f2 s = f5 s + getC s + f3 s
f3 :: Settings -> Int
f3 = liftA (* 2) getA
f4 :: Settings -> Int
f4 = liftA (+ 3) getD
f5 :: Settings -> Int
f5 = liftA2 squareAdd getB getX
where squareAdd b x = b * b + x
请注意,除了 data Settings
和 f5
。
其他内容都相同
我有一些软件设计经验,现在正在学习Haskell。在许多现实世界的软件开发中,人们面临着如下所示的情况:
假设,我有这个代码
f1 a b c d = e
where
e1 = f2 b c (f3 a)
e2 = f4 d
e = e1 + e2
f2 b c d = n + c + d
where
n = f5 b
f5 n = n*n
f3 a = a * 2
f4 a = a + 3
现在,如果我想更改 f5 使其接受另一个参数,我将不得不更改一直到 f1 的所有函数链。 它可以如下所示完成。注意添加的参数 x.
f1 a b c d x = e -- f1 needs to be changed
where
e1 = f2 b c (f3 a) x
e2 = f4 d
e = e1 + e2
f2 b c d x = n + c + d -- f2 needs to be changed
where
n = f5 b x
f5 n x = n*n -- f5 changed (**bang**)
f3 a = a * 2
f4 a = a + 3
这是做这类事情的正常 Haskell 方式还是有更好的(更多 Haskell-ish)方式? 我知道 API 中的这种更改会干扰客户端代码,但如何将影响保持在最低限度,是否有任何 Hasekll 方法?
在更一般的层面上:Haskell 在这种情况下的表现如何(特别是考虑到它的 不可变状态 特性)?在这方面它能为开发人员提供什么?还是 Haskell 本身在这方面没有任何作用,这只是一个困难的软件工程问题(没有 未来证明 这样的问题)我们要跟上吗?
我很抱歉在一个问题中问了多个问题 post,但我无能为力,因为这些问题彼此相关。另外,我找不到类似的问题,抱歉,如果我可能错过了。
正如 bhelkir 在评论中建议的那样,您可以做的一件事是将这样的参数捆绑到一个参数对象中。如果您向该对象添加一个新参数,您仍然需要更改调用 f1
的客户端代码,并更改该新参数的直接使用者(此处为 f5
),但这都是不可避免的:有人必须在某个时候提供 x
,而您需要它才能成为客户;而且你必须以某种方式消耗 x
否则你为什么要添加它?
但是,您可以避免更改像 f1
和 f2
这样的中间函数,因为它们可以忽略它们不关心的新字段。您可以通过使用 ((->) t)
的 Applicative
实例(通常称为 Reader
)来传递此对象,而不是手动执行,从而获得一些乐趣。这是一种写法:
module Test where
import Control.Applicative
data Settings = Settings {getA :: Int,
getB :: Int,
getC :: Int,
getD :: Int}
f1 :: Settings -> Int
f1 = liftA2 (+) f2 f4
-- f1 = do
-- e1 <- f2
-- e2 <- f4
-- return $ e1 + e2
f2 :: Settings -> Int
-- probably something clever with liftA3 and (+) is possible here too
f2 s = f5 s + getC s + f3 s
f3 :: Settings -> Int
f3 = liftA (* 2) getA
f4 :: Settings -> Int
f4 = liftA (+ 3) getD
f5 :: Settings -> Int
-- f5 = liftA (join (*)) getB -- perhaps a bit opaque
f5 = liftA square getB
where square b = b * b
现在,这有优点也有缺点:f1
中的逻辑(即,知道您需要用 a
调用 f3
)已经转移到 f3
本身,这将发生在任何最初读取参数并在将其传递给某个子函数之前对其进行处理的函数。这可能比原来的更清楚,或者它可能掩盖了 f1
背后的意图,具体取决于您的问题域。您始终可以更明确地编写一个函数,例如,让它修改传入的 Settings
对象以在传递它之前更改其 a
字段,就像我以 f2
为例.更一般地说,您可以以最方便的方式编写任何函数:do-notation、applicative functions 或对传入的记录对象进行普通的旧模式匹配。
但最大的优点是添加新参数非常容易:您只需向 Settings
记录添加一个字段,然后在需要它的函数中读取它:
module Test where
import Control.Applicative
data Settings = Settings {getA :: Int,
getB :: Int,
getC :: Int,
getD :: Int,
getX :: Int}
f1 :: Settings -> Int
f1 = liftA2 (+) f2 f4
-- f1 = do
-- e1 <- f2
-- e2 <- f4
-- return $ e1 + e2
f2 :: Settings -> Int
-- probably something clever with liftA3 and (+) is possible here too
f2 s = f5 s + getC s + f3 s
f3 :: Settings -> Int
f3 = liftA (* 2) getA
f4 :: Settings -> Int
f4 = liftA (+ 3) getD
f5 :: Settings -> Int
f5 = liftA2 squareAdd getB getX
where squareAdd b x = b * b + x
请注意,除了 data Settings
和 f5
。