4

I am trying to learn how do statements in haskell work. I a trying to make a very simple program where you can call a REST endpoint and execute a system command (something very simple like "ls") The problem comes with combining different actions types in a single do statement.


import Web.Scotty
import System.Cmd

main = do
  putStrLn "Starting Server"
  scotty 3000 $ do
    get "/url" $ do                         
      system "ls"
      text "Success"

But I get the next compiler error:

Main.hs:12:7:
    Couldn't match expected type ‘Web.Scotty.Internal.Types.ActionT
                                    Data.Text.Internal.Lazy.Text IO a0’
                with actual type ‘IO GHC.IO.Exception.ExitCode’
    In a stmt of a 'do' block: system "ls"
    In the second argument of ‘($)’, namely
      ‘do { system "ls";
            text "Success" }’

I am having a hard time trying to learn Haskell!

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
  • I don't have time for a full explanation/answer - which involves features of the Haskell type system that you may not have encountered yet - but this should work if you replace `system "ls"` with `liftIO $ system "ls"`. (`liftIO` is from the `Control.Monad.IO.Class` module). I can attempt an explanation when I have more time later, if someone else hasn't done so yet. – Robin Zigmond Nov 02 '19 at 10:38
  • 1
    You seem to know what the problem is - now use [`liftIO`](http://hackage.haskell.org/package/scotty-0.11.5/docs/Web-Scotty-Internal-Types.html#v:liftIO) to fix it :-) Compare [this answer](https://stackoverflow.com/a/51865577/1048572) – Bergi Nov 02 '19 at 10:38

1 Answers1

7

In Haskell, do-notation is used to chain statement-like things. A statement is some type constructor like IO applied to a particular result type. For example, the statement system "ls" has type IO ExitCode.

Other type constructors other than IO can work as statements. All that do-notation requires is that the type constructor implements the Monad interface which explains how to chain statements sensibly.

However, within a single do-block, only one type of statement is allowed! They must be all IO statements, or all ActionT Text IO statements. In your example you are mixing the two, which causes the error. Scotty's get function expects an ActionT Text IO statement:

get :: RoutePattern -> ActionM () -> ScottyM ()
-- ActionM is actually a synonym for ActionT Text IO

The good news is that there's a way to convert (the usual Haskell term is "lift") IO statements into ActionT Text IO statements. The latter are actually a kind of "decorator" (the usual Haskell term is "monad transformer") over IO actions, which enable extra functionality related to Scotty. You can "lift" IO actions into the decorator using the liftIO function, like this:

get "/url" $ do                         
     liftIO (system "ls")
     text "Success"

In general, when can we use liftIO to lift a plain IO statement into a "decorated" statement? The "decorator" type constructor must have a MonadIO instance besides the usual Monad instance. MonadIO is what provides the liftIO function.

In our case, looking at the available instances for ActionT:

(MonadIO m, ScottyError e) => MonadIO (ActionT e m)

Which means something like "if m is has a MonadIO instance—like IO trivially does—and the error type e has a ScottyError instance—like Text does—then we can lift IO statements to ActionT e m statements".

And the specialized type for liftIO is:

liftIO :: IO a -> ActionT Text IO a
danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • 1
    I'd prefer to call these "values" or "expressions", not "statements" - even if they are statement-like in `do` notation. – Bergi Nov 02 '19 at 11:08
  • 1
    @Bergi Yeah, in Haskell all "statements" are actually values like any other. In a way, the `Monad` typeclass defines what operations and laws a type must satisfy in order for its values to be statement-like. – danidiaz Nov 02 '19 at 11:21
  • 3
    @Bergi Curiously, the Haskell report uses `do { stmts }` in the reference [grammar](https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-470003.14), and `;` is called an "empty statement", so I guess the official term is statement. This is probably not the best term, for the reasons you mention. – chi Nov 02 '19 at 12:09
  • I think the idea is that monadic values in Haskell correspond to statements in imperative languages. This is clear in the case of `IO`, which is presumably what motivated the idea, but more of a stretch with other monads. For example a `State s a` value can feel quite "statement-like" (particularly those which alter the state and ignore the result value), but it would feel very awkward to describe, say, a list as a "statement". – Robin Zigmond Nov 02 '19 at 12:26
  • 1
    @ch You have to admit, there are a *lot* of things in Haskell that could do with better names :) – chepner Nov 02 '19 at 13:03
  • 1
    @RobinZigmond This is very subjective but, do-blocks in the list monad do feel like composed of statements to me! Yes, there's a difference with `IO` in that each identifier bound with a `<-` corresponds not to a single value, but to a whole level in an implicit search tree. List "statements" have the "effect" of adding new levels to the tree. I still see this as sequential, if only logically, and have trouble visualizing the search tree when not using do notation. – danidiaz Nov 02 '19 at 14:23