Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add runStmt for executing IO statements and binding new names #38

Merged
merged 1 commit into from
Sep 6, 2017

Conversation

HeinrichApfelmus
Copy link
Contributor

Hello Daniel, thanks for maintaining hint!

My project HyperHaskell is built on the hint library. In order to be as useful as the command line GHCi, I need the ability to bind names and execute statements in the IO monad, e.g. the ability to interpret things like

x <- return 42
print x

For this reason, I have implemented a wrapper around the runStmt function from the GHC API, and would like to see it included in the hint library, hence this pull request.

Copy link
Contributor

@mvdan mvdan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. Apologies for the late reply; I was travelling when this came through.

I'm not sure that I understand the purpose of this addition. Could you not accomplish the same with the existing API?

CC @gelisam

CHANGELOG.md Outdated
### 0.7.1

* Add `runStmt` for executing statements in the `IO` monad and binding new names.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't mention a new release. We don't know if it will be 0.7.1 or 0.8.0, for example.

If you want to start the new changelog entry, call it "Upcoming release".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! Sorry about this, I just wanted to reduce your burden of having to update numbers everywhere, but was apparently overly eager.

hint.cabal Outdated
@@ -1,5 +1,5 @@
name: hint
version: 0.7.0
version: 0.7.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't change this.

@HeinrichApfelmus
Copy link
Contributor Author

HeinrichApfelmus commented Aug 17, 2017

Apologies for the late reply;

No worries. 😄

I'm not sure that I understand the purpose of this addition. Could you not accomplish the same with the existing API?

No, I don't think so. The thing is that IO actions that are executed with runStmt can bind new variables in the interpreter. In other words, I can run runStmt "x <- return 42" at one point in my program, and then runStmt "print x" at a later point. The name x is not a name in my program code, it is a name bound in the interpreter context. This seems necessary to me if my program is a GHCi replacement and I want users to be able to define new names and bind values to them.

@gelisam
Copy link
Contributor

gelisam commented Aug 18, 2017

Could you not accomplish the same with the existing API?

Close, but not quite. If the host program knows the type of the variables which get bound by those statements, it's possible to construct an environment on the host program's side, and to pass this environment to every subsequent expression and statement as follows.

{-# LANGUAGE GADTs, ScopedTypeVariables #-}

import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.State (StateT)
import Data.Map (Map)
import Data.Typeable
import Language.Haskell.Interpreter
import Test.DocTest
import Text.Printf
import qualified Control.Monad.Trans.State as State
import qualified Data.Map as Map


-- a variant of 'as' for constructing function types
asFunction :: a         -- ^ value is ignored, used to specify the type
           -> b         -- ^ value is ignored, used to specify the type
           -> (a -> b)  -- ^ output is bottom, should only be used to specify a type
asFunction = undefined


data SomeTypeable where
  SomeTypeable :: Typeable a => a -> SomeTypeable

type Binding = (String, SomeTypeable)
type Env = Map String SomeTypeable
type M = InterpreterT (StateT Env IO)

interpretInEnv :: forall a. Typeable a
               => String
               -> a  -- ^ value is ignored, used to specify the type
               -> [Binding]
               -> M a
interpretInEnv expr _ [] = interpret expr (as :: a)
interpretInEnv expr _ ((var, SomeTypeable value) : bindings) = do
  let expr' = printf "(\\%s -> %s)" var expr
  f <- interpretInEnv expr' (asFunction value (as :: a)) bindings
  return (f value)

runIOStmt :: String -> M ()
runIOStmt stmt = do
  env <- lift State.get
  ioAction <- interpretInEnv stmt (as :: IO ()) (Map.toList env)
  liftIO ioAction

runBindStmt :: forall a. Typeable a
            => String
            -> String
            -> a  -- ^ value is ignored, used to specify the type
            -> M ()
runBindStmt var expr _ = do
  env <- lift State.get
  ioAction <- interpretInEnv expr (as :: IO a) (Map.toList env)
  value <- liftIO ioAction
  lift $ State.modify' $ Map.insert var $ SomeTypeable value

-- |
-- >>> testStmts
-- 42
-- Right ()
testStmts :: IO (Either InterpreterError ())
testStmts = flip State.evalStateT Map.empty
          $ runInterpreter
          $ do
  setImports ["Prelude"]
  runBindStmt "x" "return 42" (as :: Int)
  runIOStmt "print x"

But if the statements are arbitrary strings typed by the user, the host program can't know what type they have, and so the variables can't be transferred from the interpreted program to the host program. As a result, the host program can't put those values back into scope when evaluating the next expression.

With runStmt, the values are kept on the interpreted side and they automatically remain in scope for the subsequent statements and expressions of this runInterpreter block. I think it's a good feature to add.

If it was only up to me, however, I would expose the feature in a slightly more structured way. runStmt doesn't seem to be the same as typing commands in ghci; :set commands aren't supported, for example, and neither is the newer x = 5 abbreviation for let x = 5. The four formats which seem to be supported are x <- ioAction, let x = expr, and expressions and IO actions whose type has a Show instance. So I would prefer to hide the String-based encoding from the user, and to provide more precisely-typed operations instead:

defineVar :: MonadInterpreter m
          => String -- ^ var name
          -> String -- ^ expression
          -> m ()
bindVar :: MonadInterpreter m
        => String -- ^ var name
        -> String -- ^ IO expression
        -> m ()

I wouldn't provide anything special for expressions and IO actions whose type has a Show instance, it's easy enough to implement those today using the normal interpret operation.

@HeinrichApfelmus
Copy link
Contributor Author

defineVar and bindVar

Strictly speaking, runStmt also makes it possible to bind multiple variables at once via pattern matching. For example,

runStmt "(x, y) <- return (1,2)"

will bind values to both x and y.

If only defineVar and bindVar are available, one would have to introduce an additional variable, and express the code above as

bindVar "fresh" "return (1,2)"
defineVar "x" "fst fresh"
defineVar "y" "snd fresh"

Note that we have to pay attention that the intermediate variable fresh does not shadow any existing variable binding. For this reason, I would suggest to also add a utility function

freshVar :: MonadInterpreter m => m String

that generates a new unique variable name within a single interpreter session. (Of course, the library user could implement this him/herself, but in this case, I think it is justified to have it as part of the library, because dealing with variables and shadowing is something that interpreter libraries should do.)

@gelisam
Copy link
Contributor

gelisam commented Aug 18, 2017

+1 for freshVar!

The use case in which a pattern is used to bind multiple variables at once looks tricky to implement in terms of bindVar, because you'll have to parse a pattern for an unknown type. bindVar and defineVar should probably accept patterns, with variable being the pattern which will probably be used the most often. Given this expected usage, I don't think bindPattern and definePattern would be great names. Maybe just bind and define?

@HeinrichApfelmus HeinrichApfelmus force-pushed the master branch 2 times, most recently from 6cd881b to 6313092 Compare August 30, 2017 15:16
@HeinrichApfelmus
Copy link
Contributor Author

Any decision on this?

I'm happy to draw up another pull request that implements define, bind and freshVar if desired.

(I have added proper support for GHC 8.2, where the old runStmt function has been replaced by execStmt.)

@mvdan
Copy link
Contributor

mvdan commented Aug 30, 2017

Thanks for fixing the GHC 8.2 support.

@gelisam seems to understand this well, so I'll let him decide. He's a collaborator, so he can press the "rebase and merge" button if he wishes, or request changes.

@gelisam gelisam merged commit f10e357 into haskell-hint:master Sep 6, 2017
@gelisam
Copy link
Contributor

gelisam commented Sep 6, 2017

He's a collaborator, so he can press the "rebase and merge" button if he wishes

I can? I have noticed that you're a lot more conservative than I am when it comes to merging patches, so you might regret this :)

Rebased and merged!

@mvdan
Copy link
Contributor

mvdan commented Sep 6, 2017

I'm simply conservative because I don't know as much Haskell/GHC as others and because I default to not extending the API :)

Thanks for your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants