我刚读完“学到Haskell来造福大人!”。书,所以我的问题可能很幼稚。 我不明白的是如何从纯代码中调用“不纯”的IO函数。
这是一个用C#编写的工作示例。在我们的业务逻辑中,我们根据天气计划一些行动。我们以通常的C#方式进行操作。
interface IWeatherForecast
{
WeatherData GetWeather(Location location, DateTime timestamp);
}
// concrete implementation reading weather from DB
class DbWeather : IWeatherForecast
{
public override WeatherData GetWeather(Location location, DateTime timestamp)
{...}
}
class WeatherFactory
{
public IWeatherForecast GetWeatherProvider()
{...}
}
// Business logic independent from any DB
class MaritimeRoutePlanner
{
private IWeatherForecast weatherProvider = weatherFactory.GetWeatherProvider();
public bool ShouldAvoidLocation(Location location, DateTime timestamp)
{
WeatherData weather = weatherProvider.GetWeather(location, timestamp);
if(weather.Beaufort > 8)
return true;
else...
...
}
}
如何在Haskell中实现此逻辑?
实际上,“纯逻辑” MaritimeRoutePlanner调用weatherProvider.GetWeather()
,这是“纯IO”内容。
在Haskell中有可能吗?您将如何在Haskell中对此建模?
答案 0 :(得分:12)
常见问题(如何从纯函数调用不纯函数)是一个常见问题解答。参见例如这个问题及其答案:How to return a pure value from a impure method
与其他与软件体系结构相关的主题一样,如何以更具功能性的方式构造代码也取决于环境。您正在编写哪种程序? REST API?智能手机应用程序?控制台程序?批处理工作?加载项?
在许多情况下,您可以摆脱我所谓的纯-纯-纯三明治:
在Haskell中您可以执行此操作,因为入口点始终是不纯的。这是天气决策问题的简单示意图。首先定义要使用的数据。在这里,我仅包含beaufort
值,但我假设WeatherData
将包含更多数据(这就是为什么我将其定义为data
而不是newtype
的原因)。
data WeatherData = WeatherData { beaufort :: Int } deriving (Eq, Show)
您现在可以将决策逻辑编写为纯函数:
shouldAvoidLocation :: WeatherData -> Bool
shouldAvoidLocation weather = beaufort weather > 8
加载数据是一个完全具体的操作:
readWeatherFromDb :: Location -> LocalTime -> IO WeatherData
readWeatherFromDb location timestamp = -- implementation goes here...
这里没有明确的抽象。该函数读取数据并返回不纯数据。那可能是不纯-纯-不纯三明治中的第一步(不纯)。
现在可以根据该体系结构构造应用程序的入口点:
main :: IO ()
main = do
w <- readWeatherFromDb Berlin $ LocalTime (fromGregorian 2019 8 29) (TimeOfDay 8 55 8)
if shouldAvoidLocation w
then putStrLn "Avoid"
else putStrLn "Go"
调用shouldAvoidLocation
是三明治中间的好东西,然后是不纯的putStrLn
调用。
答案 1 :(得分:6)
简而言之,您不会从不纯的“函数”(又名 action )中提取数据;您将您的纯函数推入新动作。
data WeatherData = WeatherData { beaufort :: Int, ... }
-- getWeather is a pure function
-- getWeather someLocation someDate is an action
getWeather :: Location -> DateTime -> IO WeatherData
getWeather l d = ...
-- badWeather is a pure function
badWeather :: WeatherData -> Bool
badWeather wd = beaufort wd > 8
-- avoidLocation is a pure function
-- avoidLocation someLocation someDate is an action
-- We can simply use fmap to lift (or wrap) the pure function badWeather
-- into a new action.
avoidLocation :: Location -> DateTime -> IO Bool
avoidLocation l d = fmap badWeather (getWeather l d)
avoidLocation
实际上并不产生布尔值;它会创建一个动作,最终执行该动作时,会使用 badWeather
生成布尔值。
答案 2 :(得分:4)
如果效果和纯逻辑之间的交织对于基于“三明治”的解决方案而言太复杂了,那么一种选择是使用发生效果的单子参数化您的依赖关系,然后使您的逻辑多态< / em>所有单子。
例如,这是您的代码的近似翻译:
{-# LANGUAGE ExplicitForAll #-}
data WeatherData = WeatherData -- dummy type
data Location = Location -- dummy type
data DateTime = DateTime -- dummy type
newtype WeatherForecast m =
WeatherForecast { getWeather :: Location -> DateTime -> m WeatherData }
-- simply a monadic action that creates a forecast
type WeatherFactory m = m (WeatherForecast m)
-- A concrete factory that works in the IO monad
aWeatherFactory :: WeatherFactory IO
aWeatherFactory =
do putStrLn "I'm effectfully allocating a WeatherForecast!"
return
WeatherForecast {
getWeather = \_ _ ->
do putStrLn "I'm connecting to the Internet!"
return WeatherData
}
newtype MaritimeRoutePlanner m =
MaritimeRoutePlanner { shouldAvoidLocation :: m Bool }
-- The logic only knows that m is a monad and nothing more.
makeMaritimeRoutePlanner :: forall m. Monad m
=> WeatherFactory m -> MaritimeRoutePlanner m
makeMaritimeRoutePlanner forecastFactory =
MaritimeRoutePlanner {
shouldAvoidLocation =
do forecast <- forecastFactory
WeatherData <- getWeather forecast Location DateTime
return False
}
WeatherForecast
和WeatherFactory
都有一个monad的类型参数,它们的方法在其中起作用。特别是,aWeatherFactory
返回在WeatherFactory
上工作的IO
。
但是请注意forall
签名中的makeMaritimeRoutePlanner
。它强制逻辑在所有个可能的单子上工作,这意味着它不能使用任何 concrete 单子特有的功能。
使用示例:
*Main> let planner = makeMaritimeRoutePlanner aWeatherFactory
*Main> shouldAvoidLocation planner
I'm effectfully allocating a WeatherForecast!
I'm connecting to the Internet!
False
将有效的依赖关系作为参数(或作为Reader
monad的环境)进行传递是相对常见的。我认为使单子逻辑多态的另一种技巧不太流行。最终,生活在IO
中可能会太方便而无法放弃,或者至少不会造成麻烦,以至于无法解决“多态面纱”的问题。
(当然,还有其他可能的解决方案,例如free / freer monad等)。