Transparent Monads syntax and Monadic Flow Control interpretation

Now I see the root of the question, daily tiredness & suggestion as simple as the following:

Imagine the following code is correct:

Some(1) + 2 // Some[Int]](3)

or deeper one:

Future(Some("Scala")).length // Future[Some[Int]](5)

Intuitively we all know the result, its type and exactly what is the meaning of missed code (to be correct Scala now).

The second one (derived) is that we know how to correct to Scala code the sense of the following if we expect Future[Int] in the following block:

val f1 = Future(1)
val f2 = f1 + 2

f1 + f2 + 3 // Future[Int](7)

Why not to move this our evidence (exact in 99% :slight_smile: ) to language and give the compiler to do that job? Possibly, denoted by keywords like for comprehension does.
In other words: make the inevitable layer of monads as transparent as possible. Or: move monads from library to language entity layer and embed everything daily associated with monads to the syntax.

To play with the questions there are 2 macros on GitHub, which (I hope) cover the full Scala 2 syntax in all possible directions of the above root point (limited to 5 levels of nested types), including custom monads.
There is more reasoning & found limitations in GitHub article. GitHub - SerhiyShamshetdinov/sugar-tms: Transparent Monads syntax and Monadic Flow Control interpretation in Scala

Thank everyone involved for this wonderful language!

What should happen for such snippets?

Future(Seq("Scala")).length
Future(Some("Scala")).head

Generally I’m against such magic but it could have bit more sense when we will narrow context in some way.

given AutoDirectStyleFor[Future] = ???
given AutoDirectStyleFor[Option] = ???
Future(Some(1)) + 2 + Some(3) + Future(4) //Future(Some(10))
//could be translated 
Future(Some(1))
  .map(_.map(_ + 2))
  .map(_.flatMap(prev => Some(3).map(_+prev))
  .flatMap(prev => Future(4).map(x => prev.map(_ + x)))

it is in fact similar to async/await but await part is implicit. It will have similar limitations (scala-async limitations)

//> using dep "com.github.rssh::dotty-cps-async:0.9.16"
import cps._
import cps.monads.{*, given}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val f2 = async[Future] {
  await(Future(2)) + 36 + await(Future(4))
}

f2.foreach(x => println(x)) //42
  1. “What should happen for such snippets?”
    First mind is to fix “most inner” or “most outer” of the stack style for selecting target to apply method.
    But actually this may be determined by the expected result type:
Future(Seq("Scala")).length : Future[Int]       // Future(1)
Future(Seq("Scala")).length : Future[Seq[Int]]  // Future(Seq(5))

Future(Some("Scala")).head : Future[String]        // Future("Scala")
Future(Some("Scala")).head : Future[Option[Char]]  // Future(Some('S'))
  1. async/await in any way of the code implementation breaks the primary sense and beauty of the Future.
    If we “should” get sync value of the Future anywhere inside then the correct way is to get it once at the source and use its Try in that “inside”.
    Not a Subject :slight_smile: , but as to me the current steps look like we many years had very nice resilient rubber band and now (I don’t know why, “no more time” or what?) we patch this rubber by async/await braces using stapler making ideology holes in generally hidden places (in the library).

async/await still will translate above code to maps and flatMaps. there is no escape from Future there.

For more context there is new Martin’s talk about Direct style scala that is somehow related to your proposal:

1 Like

@scalway, thanks for samples. I never used ones and yes, I misunderstood these async/awaits with waiting for result style. Was inattentive, sorry.

But actual question is not only about the Future, not about how to get ride of monadic manipulations by marking the parts of code, but: about any monads and ones compositions and about possibility to write more clear and readable code still using monads.

The macros by the github-link of the header post does that. It has limitations. So, that was a question :slight_smile: Compiler may overcome some of them, possibly, if it will know what monad is.

I saw that video from conference and just after I made this post :slight_smile:

Thank you for replies

Ok, just to show what we may have the following is very short description of what these 2 macro do followed by the copy-paste sample from monadicFlowControl macro test (with tracing):

  • by transparentMonads macro: each expression of the monadic type (or of the monadic stack type) may be used in any place where value of its any inner type is expected. In other words, treat the value of the Monad[T] type as the value of type T wherever type T is expected for any depth of inner Monad[T] types recursively
  • by monadicFlowControl macro (along with above transparentMonads feature): sequence of statements is treated as joined by flatMaps-map of detected Monadic Flow Type. In other words, the statements (possibly grouped to subsequences) of the passed block are executed not just unconditionally one by one (as usual flow control) but in terms of Monadic Flow Control (being joined like in the for-comprehension header: statement (or group) execution is controlled by the previous monad)

I want to emphasize that: ones accept any monads including custom ones, even nested to 5 levels (fixable) deep, monadicFlowControl macro is able to build nested 'fors for each of nested monads and even the tree of for`s when argument requires it.

Please, pay attention to type of if argument and fields accessing.

Type checker is always happy! :slight_smile: (IDEA not always, as you may know).

Unfortunately Scala 2 only. But, I hope, full syntax.

The project was a research to feel is it possible in general and to understand the size and the scope of limitations. Project contains lots of readable test cases. Many things are really possible.

import sands.sugar.tms.TransparentMonads._
import scala.util.{Failure, Success, Try}

  case class User(id: String)
  case class UserBalance(amount: BigDecimal, currencyId: String)
  case class BalanceInfoRequest(currencyId: String)
  case class BalanceInfoResponse(currencyId: String, currencyBalance: BigDecimal)

  def sessionIsActive(sessionId: String): Try[Boolean] = Try(sessionId == "good session")
  def getUser(sessionId: String): Try[User] = Try(User("user-id"))
  def getUserBalance(userId: String): Try[UserBalance] = Try(UserBalance(10, "USD"))
  def getExchangeRate(fromCurrencyId: String, toCurrencyId: String): Try[BigDecimal] = Try(1.1)
  def saveAudit(sessionId: String, request: BalanceInfoRequest, response: BalanceInfoResponse): Try[Unit] = Try({})
  def raiseError(exception: Exception): Failure[Nothing] = Failure(exception)


      def getBalanceInfoResponse(sessionId: String, request: BalanceInfoRequest): Try[BalanceInfoResponse] = 
        monadicFlowControl[Try[BalanceInfoResponse]] {
          val user = if (sessionIsActive(sessionId)) getUser(sessionId) else raiseError(new Exception("Bad session"))
          val userBalance = getUserBalance(user.id)
          val currencyBalance = if (userBalance.currencyId == request.currencyId)
            userBalance.amount
          else
            userBalance.amount * getExchangeRate(userBalance.currencyId, request.currencyId)

          val response = BalanceInfoResponse(request.currencyId, currencyBalance)

          saveAudit(sessionId, request, response)

          response
        }

      //Typechecked code with dropped packages:
      //[debug] * tms INPUT code.tree:
      {
        val user = if (tts1[Try, Boolean](sessionIsActive(sessionId)))
          getUser(sessionId)
        else
          raiseError(new Exception("Bad session"));
        val userBalance = getUserBalance(tts1[Try, User](user).id);
        val currencyBalance = if (tts1[Try, UserBalance](userBalance).currencyId.==(request.currencyId))
          tts1[Try, UserBalance](userBalance).amount
        else
          tts1[Try, UserBalance](userBalance).amount.*(tts1[Try, BigDecimal](getExchangeRate(tts1[Try, UserBalance](userBalance).currencyId, request.currencyId)));
        val response = BalanceInfoResponse.apply(request.currencyId, currencyBalance);
        saveAudit(sessionId, request, response);
        response
      }
    
      //[debug] * tms extracted fors code view:
      {
        for {
          valueOfTry$macro$1 <- sessionIsActive(sessionId)
          user = if (valueOfTry$macro$1)
            getUser(sessionId)
          else
            raiseError(new Exception("Bad session"))
          userValue$macro$2 <- user
          userBalance = getUserBalance(userValue$macro$2.id)
          userBalanceValue$macro$3 <- userBalance
          valueOfTry$macro$4 <- getExchangeRate(userBalanceValue$macro$3.currencyId, request.currencyId)
          currencyBalance = if (userBalanceValue$macro$3.currencyId.$eq$eq(request.currencyId))
            userBalanceValue$macro$3.amount
          else
            userBalanceValue$macro$3.amount.$times(valueOfTry$macro$4)
          response = BalanceInfoResponse.apply(request.currencyId, currencyBalance)
          wcMf$macro$5 <- saveAudit(sessionId, request, response)
        } yield {
          response
        }
      }
    
      // FINAL result
      // [debug] * tms postprocessed fors code view:
      {
        for {
          valueOfTry$macro$1 <- sessionIsActive(sessionId)
          userValue$macro$2 <- if (valueOfTry$macro$1)
            getUser(sessionId)
          else
            raiseError(new Exception("Bad session"))
          userBalanceValue$macro$3 <- getUserBalance(userValue$macro$2.id)
          valueOfTry$macro$4 <- getExchangeRate(userBalanceValue$macro$3.currencyId, request.currencyId)
          currencyBalance = if (userBalanceValue$macro$3.currencyId.$eq$eq(request.currencyId))
            userBalanceValue$macro$3.amount
          else
            userBalanceValue$macro$3.amount.$times(valueOfTry$macro$4)
          response = BalanceInfoResponse.apply(request.currencyId, currencyBalance)
          wcMf$macro$5 <- saveAudit(sessionId, request, response)
        } yield {
          response
        }
      }

The root question is the same: will Scala be able to handle Some(1) + 2 ?