Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call impure functions from pure ones?

I have just finished reading a "Learn You a Haskell for Great Good!" book so my question can be very naive. What I don't understand is how to call "impure" IO functions from the pure code.

Here is a working example written in C#. In our business logic we plan some actions based on weather. We do it in usual C# manner.

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

How do I implement this logic in Haskell?

In reality "pure logical" MaritimeRoutePlanner calls weatherProvider.GetWeather() which is "impure IO" stuff.

Is it possible in Haskell? How would you model this in Haskell?

like image 742
Stefan Dorn Avatar asked Aug 29 '19 06:08

Stefan Dorn


3 Answers

The general question (how to call an impure function from a pure function) is a FAQ. See e.g. this question and its answers: How to return a pure value from a impure method

How you structure code in a more functional manner depends, like any other topic related to software architecture, on circumstances. What sort of program are you writing? A REST API? A smartphone app? A console program? A batch job? An add-in?

In many cases, you can get away with what I call an impure-pure-impure sandwich:

  1. Gather all required data from impure sources
  2. Call a pure function with that data
  3. Do something impure with the return value from the pure function

In Haskell you can do this because entry points are always impure. Here's a simple sketch of the weather decision problem. You start by defining the data on which you'll work. Here, I only include the beaufort value, but I assume that WeatherData would include more data than that (which is why I define it as data and not as a newtype).

data WeatherData = WeatherData { beaufort :: Int } deriving (Eq, Show)

You can now write the decision logic as a pure function:

shouldAvoidLocation :: WeatherData -> Bool
shouldAvoidLocation weather = beaufort weather > 8

Loading data is an entirely concrete operation:

readWeatherFromDb :: Location -> LocalTime -> IO WeatherData
readWeatherFromDb location timestamp = -- implementation goes here...

There's no explicit abstraction in place here. This function reads data and returns impure data. That could be the first (impure) step in the impure-pure-impure sandwich.

The entry point of the application can now be structured according to that architecture:

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"

The call to shouldAvoidLocation is the good, pure stuff in the middle of the sandwich, which is then followed by impure putStrLn calls.

like image 62
Mark Seemann Avatar answered Oct 20 '22 06:10

Mark Seemann


In short, you don't pull data from an impure "function" (aka action); you push your pure function into a new 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 doesn't actually produce a Boolean value; it creates an action that, when finally executed, uses badWeather to produce a Boolean value.

like image 5
chepner Avatar answered Oct 20 '22 07:10

chepner


If the intertwining between effects and pure logic is too complex for a "sandwich"-based solution, one option is to parameterize your dependencies with the monad in which their effects take place, and then make your logic polymorphic over all monads.

For example, here's an approximate translation of your code:

{-# 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
    }

Both WeatherForecast and WeatherFactory have a type parameter for the monad in which their methods have effects. In particular, aWeatherFactory returns a WeatherFactory that works over IO.

But notice the forall in the signature of makeMaritimeRoutePlanner. It forces the logic to work over all possible monads, which means it can't use functionality particular to any concrete monad.

An example of use:

*Main> let planner = makeMaritimeRoutePlanner aWeatherFactory
*Main> shouldAvoidLocation planner
I'm effectfully allocating a WeatherForecast!
I'm connecting to the Internet!
False

Passing your effectful dependencies as parameters (or as the environment of a Reader monad) is relatively common. The further trick of making the logic polymorphic over the monad is less popular, I think. Ultimately, living in IO might be too convenient to forgo, or at least not problematic enough to bother putting the "veil of polymorphism" in place.

(Of course, there are other possible solutions like free/freer monads and the like.)

like image 4
danidiaz Avatar answered Oct 20 '22 06:10

danidiaz