开发者

Do notation without monads: possible?

开发者 https://www.devze.com 2023-03-14 18:10 出处:网络
I have a stateful type with the >> and >>= operators, which is nearly a monad. The intended use is to generate code for another language, and having the do-notation available will be very

I have a stateful type with the >> and >>= operators, which is nearly a monad. The intended use is to generate code for another language, and having the do-notation available will be very appropriate.

However, there is no well-defined return function, as values are only supposed to be produced along with side-effects. So, if I fake a return function, the return function should return an error only, and it breaks the monad laws. ( I.e. it is never valid to use a return function. )

What I observe is that the do-notation only sugars two operators, the >> and >>= operators.

Is there a way to retain something like the do notation for only those 2 operators, or something as clean as it, but without making a monad?

NOTE: Below the line are details that are not necessary for the question in the title. It's the context for why I asked the question, replies to which I'm interested in. Others who browse this topic would probably prefer to ignore it.


CORRECTION: I'm not generating code into an imperative language, although it shouldn't matter. I'm generating code into Javascript.

CLARIFICATION (in response to bdonlan): My type is (m a), which carries a state (which is the code to be generated given various parameters, similar to the state monad) and will return a value (the variable names in the generated code, with a haskell type attached). Thus, there shouldn't be a way to return values, which are associated with variable names, without generating code that defines the variable names. * This is solely due to my design, which might be inferior to others which I have not thought of.

In response to the responses: There seems to be two kinds of responses to this. First, is whether it is indeed possible, and perhaps the best way to go about using the do notation. The second is on whether it is better to use a monad, and define a return (or whether it even makes sense to not do so - perhaps at a later point in time, I would find that a return is required).

Example of wha开发者_如何转开发t I would be doing:

data JsState = JsState { code :: String , vidCount :: Int } 
-- vidCount is for generating unique variable names

newtype JsStmt = JsStmt ( JsState -> JsState )

newtype JsMonad a = JsMonad ( JsState -> ( a , JsState ) )

def :: (JsValue a) => a -> JsMonad a

-- Outputs "var " ++ toUniqueVarName vidCount ++ " = " toCode a ++ ";"

-- Returns JsMonad with the variable name as the value,
-- with a type attached, and the JsState's vidCount is incremented by 1.


alertJsInt :: JsIntE -> JsMonad ()

-- Outputs something like "alert(" ++ toCode JsIntE ++ ");"

do
  x <- def $ JsIntL 2
  y <- def $ JsIntL 4
  alertJsInt (x + y)


Well, the sort of structure you're working with does already exist; a relevant type class can be found on Hackage, in fact. I don't recommend trying to force it into an instance of Monad, though, for the reasons that bdonlan gives. The existence of return is pretty fundamental and you're likely to run into trouble everywhere trying to use standard functions for Monads.

....however! That said, if you really want do notation, consider this quote from the GHC user guide:

"Do" notation is translated using whatever functions (>>=), (>>), and fail, are in scope (not the Prelude versions).

...in other words, you're right that using do notation makes sense, because nowhere does it actually use return when desugared. That section is about the RebindableSyntax extension, which does exactly what it claims. If you don't mind giving up do notation for Monad, you can use that extension, define your own functions, and use do notation all you like.


The idiomatic solution would be to use two monads Writer and State - Writer to accumulate the code "String" and State to track the variable counter.

You can combine the two monads into one amalgamated monad if you don't want to use a monad transformer library:

-- There are better types to use rather than String...
type Output = String
type St  = Int

newtype JsMonad a = JSMonad { getJSMonad :: St -> (a, St, Output) }

[As you are using the standard combination of two standard monads the obvious definition of return and bind won't break the monad laws.]

This is quite a common pattern for building embedded DSLs that use the do-notation, see Andy Gill's Dot library on Hackage for instance. Oleg Kiselyov also has an XML library in this style (CSXML - not on Hackage).

As you don't care about about the final answer, it is common to write a "run" function that ignores it:

runJS :: JSmonad a -> Output
runJS ma = post $ getJSMonad ma 0
  where
    post (_,_,w) = w

One thing to note is that the output code is just a "log" - you cannot inspect it as you are building it, so this does limit some of the things you can do.


It would be very difficult to use do notation without return. Consider the common pattern of:

foo = do
    x <- bar
    y <- quux
    return $ x + y

Without return it is impossible to define this. Recall that >>= has type (>>=) :: Monad m => m a -> (a -> m b) - it must return a value inside the monad. So even if we write this out without do notation, we have the same problem:

foo = bar >>= \x -> quux >>= \y -> (???????) (x + y)

So it's not a good idea to use do-notation for this. Particularly, there's nothing to stop the compiler implementation from introducing returns with transformations conforming to the monad laws (which assume the presence of return).

Moreover, even defining >>= will be difficult for you. How will you translate it to your imperative language? Remember that >>= takes an arbitrary function on its right argument; you are unable to inspect this function to tell what it might do, and thus cannot translate the body of this function into an imperative language. I suppose you could constrain the type of the argument to some kind of traced datatype and try to figure out the effects of the function, but now this means the function can't inspect its argument. In short, it's just not going to work very well.

One alternate approach you might consider is to create a Monad that represents the process of building an imperative function. It might, for example, look like this:

emitAddAndPrint varA varB = do
    varTmp <- allocateTempVariable
    emitOp (Add varTmp varA varB)
-- if you want to be fancy, emitOp (varTmp :=: varA :+: varB) or something
    emitCall "print" [varTmp]


It sounds like you have an arrow. In a monad you need to be able to write:

do
   x <- return $ f y
   if x then something else notSomething

The "if" condition gets evaluated as part of the evaluation of the "do", and the result is either "something" or "notSomething", but not both at once. However for code generation you probably want the "if" to be translated into your generated code with both branches evaluated to produce code that can make the choice at its run-time.

The equivalent code for an arrow desugars into use of the the ArrowChoice class in which you have access to both the condition and both branches, which is what you need here.


If you can make your type an instance of Monad, even with a missing return then the do notation works. I've done it (e.g.BASIC), and it's an excellent way to get imperative notation for a DSL.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号