Closed jvoigtlaender closed 1 year ago
I do not yet see how a Monad
instance would look like in that case.
instance Monad m => LangM' m where
LangM x >>= f = ???
For defining bind type constraints are: x :: m (Language -> a)
and f :: a -> m (Language -> b)
. In order to combine them we would require a Language
(as otherwise we are not able to get something of type a
), but which Language
would it be in this context there is none available? Or am I missing something?
[This could become irrelevant if #3 is applicable and resolved]
Yes, it's possible that when I had the above question/idea, that was somehow intertwined with my thinking that maybe Applicative
would be enough. That is, maybe we don't need LangM' m
to be monadic, only applicative (even if m
is a monad and should stay so).
Actually, defining a monadic bind for /\a. M (Language -> a)
should be possible, but is maybe not what one wants.
In x >>= f
we would have at hand:
x :: M (Language -> A)
f :: A -> M (Language -> B)
and would want to create a y :: M (Language -> B)
from them.
Where A
and B
are arbitrary (but fixed) types, and M
is some arbitrary (but fixed) monad.
This seems possible, at least type-wise:
y = do
(g :: Language -> A) <- x
let (h :: Language -> M (Language -> B)) = f . g
let (k :: Language -> M B) = \lang -> fmap ($ lang) (h lang)
distribute k
where
distribute :: Monad m => (Language -> m b) -> m (Language -> b)
distribute k = do
b1 <- k English
b2 <- k German
return $ \case { English -> b1; German -> b2}
That kind of distributivity (between the monad m
and the environment monad Language ->
) should hold for each finite input type (here, Language
), but it kind of "duplicates" effects (the lines where both k English
and k German
are issued, instead of selectively only one of them). Maybe it violates some of the equational laws that are required of https://en.wikipedia.org/wiki/Distributive_law_between_monads.
I gave this a bit more thought based on the current usage. The initial idea was to not constrain very much and thus allowing basically any underlying Monad and being able to retrieve the desired language in multiple ways (depending on the implementation). But of course, basing the Monadic actions on the Language should not be desirable. In fact, the default instance of OutputMonad
for IO
makes usage of the language in a hacky way:
https://github.com/fmidue/output-monad/blob/c953b14c2b73dfb9cb5749c999cdc63702b8cb8c/output-monad/src/Control/Monad/Output.hs#L355-L381 For the Autotool-Implementation usage of the ReportT
MonadTransformer is being made. https://github.com/fmidue/output-monad/blob/c953b14c2b73dfb9cb5749c999cdc63702b8cb8c/output-monad/src/Control/Monad/Report.hs#L34-L40 I guess this should be the foundation for any LangM
implementation. Here the WriterMonad (Transformer) is the Foundation for Language specific output. Theoretically this could be based on something else, e.g. a StateMonad (for instance using a Stack, therefore I introduced the flexibility originally).
I could go even further and change the type of LangM'
into
newtype LangM' w m a = LangM { unLangM :: ReportT w m a}
and thus enforce the usage of the WriterMonad
. (In fact this would make LangM'
redundant.).
But maybe this goes to far (and requires changing the types for all functions using the OutputMonad
).
But the result should not depend on the Language either, so the type could be as simple as
newtype LangM' m a = LangM { unLangM :: m a}
But this type does not reflect that Language should (somehow) be incorporated into the monad. Should this be made more explicit? Like
newtype LangM' m a = LangM { unLangM :: m Language a}
(requiring amendment of the ReportT
type)
I added some changes to the branch https://github.com/fmidue/output-monad/tree/flexible-monad. Where I basically abstracted the LangM
data type by introducing a parameter for a language data type. This is basically the flexibility the OutputMonad
required as there might be instances for different language data types. LangM
does not depend on the Language directly, but having a phantom type enables typing while adding the flexibility. In order to maintain compatibility the more general OutputMonad
is called GenericOutputMonad
while OutputMonad
simply refers to one using Language
and should in turn enable switching the library versions while keeping "user code". This is only partial true, because of the switch to Applicative
(see #3) and thus the requirement of ApplicativeDo
the Language extension has to be enabled and pure ()
needs to be added probably at the end of every applicative do block. Furthermore imports might require amendment. Also instances of OutputMonad
need to be amended as now instances for GenericOutputMonad
need to be defined. There were some type changes as well, required because of the switch to Applicative (#3) and because of the removal of Language as parameter. This will have an impact on instances. For instance the default instance for IO now explicitly needs to handle multilanguge input internally (I changed the type and used GenericReportT
here for handling the situation - but basically using any kind of states would have been an option as well).
https://github.com/fmidue/output-monad/blob/6f5f93d6f9248d275b99a6fb5efa3957f7945c1b/output-monad/src/Control/Monad/Output.hs#L446-L476
As withLang
got removed in the process (it is somehow reintroduced as https://github.com/fmidue/output-monad/blob/6f5f93d6f9248d275b99a6fb5efa3957f7945c1b/output-monad/src/Control/Monad/Output.hs#L346-L354) there was an desire to privide a guidance on how evaluation should be performed. That is why there is the type class RunnableOutputMonad
. I tried to think on how closely it is tied to the OutputMonad itself, I decided on a separate type class but with a type class constraint: https://github.com/fmidue/output-monad/blob/6f5f93d6f9248d275b99a6fb5efa3957f7945c1b/output-monad/src/Control/Monad/Output.hs#L321-L328
withLang
depends on an instance of this type class. For some testing code it might be the case that no change is required (but changing instances), but depending on the inner monad, evaluation might now need to use runLangM
.
The question arises (@owestphal), if runIsolated
can still be used as intended or if amendment is required.
Next steps would be:
Generic
and "non-generic" versions)Working with the changed interface inspired further changes which are partially benefitial and partially required for useful creation and use of instances.
The change 9a8403f0c2ee915148c62bef22daa4908a33aa8f provides functions which can be used when combining monadic (usually IO) actions and the Applicative LangM
. The common oporator a user of the library would be required to use is $=<<
(the similarity to =<<
is not chosen by accident). As LangM
is no longer monadic actions have to be lifted into the LangM
context. This is what this operator is for. Other operators are $>>
and $>>=
.
The change c238d4228344473aa0006dab72ef9db3e7bc4c17 on the other hand removes functions from the interface of OutputMonad
which are usually monadic (and therefore do not fit to #3). These functions should usually not be used in libraries (unless they wanted to make use of the power of the monad) but were used internally. For compatibility with Autotool the function translatedCode
was introduced. This function is in the same way dual to code
as translated
is to text
. In fact one could think about having more functions like this for latex
, image
, ... (i.e. different LaTeX-Code depending on the Language or different Graphics depending on the Language). code
could be seen as a special version of translatedCode
so it could be enough to express everything via translatedCode
and strip code
from the interface. But I decided to not change the interface further (yet).
In my view this completes the conceptual amendment of LangM
(and it remains the cosmetical of reordering modules).
There is one caveat on the operators: $>>=
and $=<<
are not dual in their type. The reason is that I do not want users to lift their IO operations additionally to LangM
. But for the other operator the "usual" use usually applies.
To sketch required changes when this changes becomes life i provide a diff for a typical module using OutputMonad
diff --git a/src/Modelling/ActivityDiagram/MatchAD.hs b/src/Modelling/ActivityDiagram/MatchAD.hs
index e205c29..179633f 100644
--- a/src/Modelling/ActivityDiagram/MatchAD.hs
+++ b/src/Modelling/ActivityDiagram/MatchAD.hs
@@ -1,4 +1,6 @@
+{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TupleSections #-}
@@ -35,9 +37,11 @@ import Modelling.ActivityDiagram.Auxiliary.Util (failWith, headWithErr)
import Control.Applicative (Alternative ((<|>)))
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Output (
+ GenericOutputMonad (..),
LangM,
Rated,
- OutputMonad (..),
+ OutputMonad,
+ ($=<<),
english,
german,
translate,
@@ -147,11 +151,11 @@ matchADTask
-> MatchADInstance
-> LangM m
matchADTask path task = do
- ad <- liftIO $ drawADToFile path (plantUMLConf task) $ activityDiagram task
paragraph $ translate $ do
english "Consider the following activity diagram."
german "Betrachten Sie das folgende Aktivitätsdiagramm."
- image ad
+ image $=<< liftIO
+ $ drawADToFile path (plantUMLConf task) $ activityDiagram task
paragraph $ translate $ do
english [i|State the names of all actions, the names of all object nodes, and the number
of each other type of component for the given diagram.|]
@@ -162,6 +166,8 @@ aller anderen Arten von Komponenten für das gegebene Aktivitätsdiagramm an.|]
english [i|To do this, enter your answer as in the following example.|]
german [i|Geben Sie dazu Ihre Antwort wie im folgenden Beispiel an.|]
code $ show matchADInitial
+ pure ()
+ pure ()
matchADInitial :: MatchADSolution
matchADInitial = MatchADSolution {
In situations as these: the operators allow for a straight forward translation (this should be the case for code generating advanced feedback only):
executeIO
:: (MonadIO m, OutputMonad m, Show a, Show k, Ord a, Ord k)
@@ -128,30 +136,25 @@ executeIO
-> a
-> State k
-> LangM' m (State k)
-executeIO path cmd i n t z0 = do
- z2 <- execute n t z0
- g <- drawToFile False path cmd i $ n {start = z2}
- image g
- return z2
+executeIO path cmd i n t z0 = execute n t z0
+ $>>= \z2 -> drawToFile False path cmd i (n {start = z2})
+ $>>= \g -> image g
+ $>>= pure (pure z2)
Code implementing instances or generating output; maybe eben some tests might require more changes.
One more remark: The question arose if the RunnableOutputMonad
should be stripped as interface. Reason is that usually the code handling the rendering of output is quite individual. The current interface does not fit for the Autotool-Implementation (runLangMReportMultiLang
is used there). So providing runLangMReportMultiLang
and runLangMReport
might be enough to "point in the right direction". (Cost would be: loosing withLang
which in turn could be defined locally as well.)
RunnableOutputMonad
and withLang
were put into the Generic
module which has to be imported separately (also the monadic operators beside $=<<
reside only in that module).
I've been wondering whether the generality of this type: https://github.com/fmidue/output-monad/blob/5f494e2bd2b0835254529315bc5886932071c23f/output-monad/src/Control/Monad/Output.hs#L223 is really needed/desirable.
It means that the monadic action can depend on the output language.
If instead the "character" of the action should never depend on the output language (i.e., only the encapsulated text may), then the type definition
could be used.