如何从纯函数调用不纯函数?

时间:2019-08-29 06:04:42

标签: haskell functional-programming

我刚读完“学到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中对此建模?

3 个答案:

答案 0 :(得分:12)

常见问题(如何从纯函数调用不纯函数)是一个常见问题解答。参见例如这个问题及其答案:How to return a pure value from a impure method

与其他与软件体系结构相关的主题一样,如何以更具功能性的方式构造代码也取决于环境。您正在编写哪种程序? REST API?智能手机应用程序?控制台程序?批处理工作?加载项?

在许多情况下,您可以摆脱我所谓的纯-纯-纯三明治

  1. 从不纯来源收集所有必需的数据
  2. 使用该数据调用纯函数
  3. 对纯函数的返回值做一些不纯的事情

在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
    }

WeatherForecastWeatherFactory都有一个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等)。