fmidue / output-blocks

0 stars 0 forks source link

semantics of `LangM` #2

Closed jvoigtlaender closed 1 year ago

jvoigtlaender commented 1 year ago

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

newtype LangM' m a = LangM { unLangM :: m (Language -> a)}

could be used.

marcellussiegburg commented 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]

jvoigtlaender commented 1 year ago

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).

jvoigtlaender commented 1 year ago

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:

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.

marcellussiegburg commented 1 year ago

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)

marcellussiegburg commented 1 year ago

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:

marcellussiegburg commented 1 year ago

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.

marcellussiegburg commented 1 year ago

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.

marcellussiegburg commented 1 year ago

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.)

marcellussiegburg commented 1 year ago

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).