3

I was writing a basic CRUD app in Haskell, using libraries Servant and Opaleye.

Servant to setup the API endpoints and Opaleye to store the data in DB.

Let's say there's an endpoint GET /users which returns the list of all users from DB and another endpoint POST /user which creates a new user and saves it in DB.

The program beings by initiating a connection to the DB and then it passes around this connection as a parameter to these API endpoint functions (setup using Servant) as a parameter.

Somebody recommended me that a better way is to use the Reader Monad and store the connection in the environment.

I was able to do it but what I don't get is why is Reader Monad a preferred way of sharing environment rather than directly passing arguments.

P.S. - Being a beginner in Haskell, I can use Monads, follow the tutorials and make my program run but I don't really know the beautiful hidden mathematics behind them. Which is why, I want to avoid using monads (until the time I completely comprehend the idea behind monads).

Here's my code, btw.

kishlaya
  • 453
  • 2
  • 14
  • 1
    Tangential note: "Which is why, I want to avoid using monads (until the time I completely comprehend the idea behind monads)." -- I suggest you just use them instead. The beautiful mathematics can wait. – duplode Jun 15 '18 at 15:49
  • 1
    In fact, _using_ monads [may be the best path towards comprehending the beautiful mathematics](https://byorgey.wordpress.com/2009/01/12/abstraction-intuition-and-the-monad-tutorial-fallacy/). – leftaroundabout Jun 15 '18 at 16:12
  • 3
    Not all experienced haskellers prefer using `Reader` to passing arguments: https://www.reddit.com/r/haskell/comments/8p6rjv/guidelines_for_effect_handling_in_cardano_sl/e09eeao/ – danidiaz Jun 15 '18 at 16:31
  • @danidiaz is there any _W_ such that **all** experienced users of _W_ prefer using _X_ for doing _Y_? Sure, most things have a tradeoff. IMO the benefits tend to be greater in case of `Reader`. – leftaroundabout Jun 15 '18 at 16:45
  • @leftaroundabout could you please write an answer with benefits/costs analysis? Actually "using monads is the best path..." is a good argument, though I wonder what are other benefits and drawbacks. – Yuras Jun 15 '18 at 17:05
  • @duplode Yeah I can but I find it a little uncomfortable to use something which I don't fully understand – kishlaya Jun 18 '18 at 03:49
  • @kishlaya While I sympathise, it is good to keep in mind that understanding isn't monolithic. At one level, `Monad` is just a programming interface that is applicable in many situations. While knowing about the theory lurking behind it will refine your appreciation and sharpen your intuition, the opposite is also true: theory tends to be easier to make sense of when motivated by concrete situations where it shows up. When it comes to learning Haskell, it tends to work better to pick up the mathematical underpinnings as you go, rather than taking them as prerequisites. – duplode Jun 19 '18 at 05:59
  • @duplode Yeah, I agree with that. I have started playing Monads already. Thanks! – kishlaya Jun 19 '18 at 12:44

1 Answers1

3
  1. Monad Reader is just more convenient, when you want pass arguments several levels deeper into call stack.

  2. Monad Reader facilate code change/extension. Suppose you want to fetch some value of type Foo from database, update it (in impure way) and store it back. Here are two versions, with Reader and with explicit argument passing.

    data Foo = ...
    modifyFoo :: Foo -> IO Foo
    type Handler a = Reader Connection IO a
    
    fetch1 :: Connection -> Int -> IO Foo
    fetch2 :: Int -> Handler Foo
    
    store1 :: Connection -> Foo -> IO ()
    store2 :: Foo -> Handler ()
    
    modify1 :: Connection -> Int -> IO ()
    modify1 conn key = do
      prev <- fetch1 conn key
      new  <- modify prev
      store1 conn new
    
    modify2 :: Int -> Handler ()
    modify2 key = do
      prev <- fetch2 key
      new  <- liftIO $ modify prev
      store2 new
    
    -- for brave souls
    modify2' :: Int -> Handler ()
    modify2' = fetch2 >=> liftIO . modify >=> store2
    

Should some day fetch2 and store2 change argument from Connection to something else (or bigger), you would just update Handler type alias, modify2 stays the same. In case modify1, Connection is explicit in type signature, you would have to change it too.

For another example of usage of Reader I would suggest xmonad window manager. There is XConfig datatype somewhere in internals of X monad, but most of the time I do not want to know it, leave alone pass it along.

iu-guest
  • 46
  • 2
  • Just like with `Handler`, you can make `Connection` a type alias (e.g. `modify1 :: Handler -> Int -> IO`), so there is no benefits/costs in #2. And #1 is a bit vague, could you please elaborate. – Yuras Jun 15 '18 at 16:56
  • I agree about #2, except... it is okay to make alias to `Connection` if it was project-defined datatype. If data `Connection` comes from some library, making it alias would cause confusion: ``type Connection = (Lib.Connection, Bar)`` is rather incomprehensive. – iu-guest Jun 16 '18 at 05:33