Direct Style Effect Systems -�The Print[A] Example�- A Comprehension Aid

pjschwarz 96 views 37 slides May 06, 2024
Slide 1
Slide 1 of 37
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37

About This Presentation

The subject of this deck is the small Print[A] program in the following blog post by Noel Welsh: https://www.inner-product.com/posts/direct-style-effects/.

Keywords: "direct-style", "context function", "context functions", "algebraic effect", "algebraic ...


Slide Content

Direct Style Effect Systems
The Print[A] Example
A Comprehension Aid
Part 1
Context is King
Noel Welsh
@noelwelsh
Direct-style Effects Explained
Adam Warski
@adamwarskiDaniel Westheide
@kaffeecoder
The type of a singleton object
based on
@philip_schwarzslides byhttps://fpilluminated.com/
=> ?=>

Noel Welsh
@noelwelsh
HaveyoureadthisblogpostbyNoelWelsh?
https://www.inner-product.com/posts/direct-style-effects/
Direct-styleeffects,alsoknownasalgebraiceffectsandeffect
handlers,arethenextbigthinginprogramminglanguages.
AtthesametimeIseesomeconfusionaboutdirect-styleeffects.In
thispostIwanttoaddressthisconfusionbyexplainingthewhat,the
why,andthehowofdirect-styleeffectsusingaScala3
implementationasanexample.

Noel Welsh is, of course, the co-author of Scala with Cats, and author of the upcoming Functional Programming Strategies
Noel Welsh
@noelwelsh

•WhatWeCareAbout
•DirectandMonadicStyle
•Description and Action
•ReasoningandComposingwithEffects
•TheDesignSpaceofEffectSystems
•Direct-styleEffectSystemsinScala3
•CompositionofDirect-StyleEffects
•EffectsThatChangeControlFlow
•Capturing,Types,andEffects
•ConclusionsandFurtherReading
ThesubjectofthisdeckisthesmallPrint[A]programseenintheblogpostsectionhighlightedbelow.
Inordertounderstandtheprogram,Ihadtofilltwolacunae.Thefirstonewasminor,buttheotheronewasnot,andfillingitwaslong
overdue.Inthepassagebelow,NoelkindofalludestoitwhenhespeaksofmachinerythatisnewinScala3andisprobablyunfamiliar
tomanyreaders.
Theintentionbehindthisdeckistosharethefollowing:
1.ThetwobitsofknowledgethatIusedtofilltheabovelacunae.IacquiredthefirstonefromabookwrittenbyDanielWestheide,
andthesecondonefromablogpostbyAdamWarski.
2.TheprocessthatIfollowedtoreachanunderstandingoftheprogram,incasesomeoneelsefindsbitsofituseful.
Noel Welsh
@noelwelsh
Direct-style Effect Systems in Scala 3
Let’snowimplementadirect-styleeffectsysteminScala3.ThisrequiressomemachinerythatisnewinScala3.
Sincethat’sprobablyunfamiliartomanyreaderswe’regoingtostartwithanexample,explaintheprogramming
techniquesbehindit,andthenexplaintheconceptsitembodies.
Ourexampleisasimpleeffectsystemforprintingtotheconsole.Theimplementationisbelow.Youcansave
thisinafile(called,say,Print.scala)andrunitwithscala-cliwiththecommandscala-cliPrint.scala.

Beforewegetstarted,weneedascontexttheblogpostsection
highlightedbelow.Seenextslideforthecontentsofthatsection.
•WhatWeCareAbout
•DirectandMonadicStyle
•DescriptionandAction
•ReasoningandComposingwithEffects
•TheDesignSpaceofEffectSystems
•Direct-style Effect Systems in Scala 3
•CompositionofDirect-StyleEffects
•EffectsThatChangeControlFlow
•Capturing,Types,andEffects
•ConclusionsandFurtherReading

Noel Welsh
@noelwelsh
DescriptionandAction
Anyeffectsystemmusthaveaseparationbetweendescribingtheeffectsthatshouldoccur,andactuallycarryingoutthose
effects.Thisisarequirementofcomposition.Considerperhapsthesimplesteffectinanyprogramminglanguage:printingtothe
console.InScalawecanaccomplishthisasasideeffectwithprintln:
println("OMG, it's an effect")
Imaginewewanttocomposetheeffectofprintingtotheconsolewiththeeffectthatchangesthecolorofthetextonthe
console.Withtheprintlnsideeffectwecannotdothis.Oncewecallprintlntheoutputisalreadyprinted;thereisnoopportunity
tochangethecolor.
Letmebeclearthatthegoaliscomposition.Wecancertainlyusetwosideeffectsthathappentooccurinthecorrectorderto
gettheoutputwiththecolorwewant.
println("\u001b[91m") // Color code for bright red text
println("OMG, it's an effect")
Howeverthisisnotthesamethingascomposinganeffectthatcombinesthesetwoeffects.Forexample,theexampleabove
doesn’tresettheforegroundcolorsoallsubsequentoutputwillbebrightred.Thisistheclassicproblemofsideeffects:they
have“actionatadistance”meaningonepartoftheprogramcanchangethemeaningofanotherpartoftheprogram.Thisin
turnsmeanswecannotreasonlocally,norcanwebuildprogramsinacompositionalway.
Whatwereallywantistowritecodelike
Effect.println("OMG, it's an effect").foregroundBrightRed
whichlimitstheforegroundcolourtojustthegiventext.Wecanonlydothatifwehaveaseparationbetweendescribingthe
effect,aswehavedoneabove,andactuallyrunningit.

Next,let’stakeaquickglanceatNoel’sprogram,
justtogetanideaofitssizeandconstituentparts.

@main defgo(): Unit= {
// Declare some `Prints`
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
// Composition
valred: Print[Unit] =
Print.println("Amazing!").prefix(Print.print("> ").red)
// Make some output
Print.run(message)
Print.run(red)
}
objectPrint{
defprint(msg: Any)(usingc: Console): Unit=
c.print(msg)
defprintln(msg: Any)(usingc: Console): Unit=
c.println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for `Print` values */
inlinedefapply[A](inlinebody: Console?=> A): Print[A] =
body
}
extension[A](print: Print[A]) {
/** Insert a prefix before `print` */
defprefix(first: Print[Unit]): Print[A] =
Print{
first
print
}
/** Use red foreground color when printing */
defred: Print[A] =
Print{
Print.print(Console.RED)
valresult = print
Print.print(Console.RESET)
result
}
}
typePrint[A] = Console?=> A
// For convenience, so we don't have
// to write Console.type everywhere.
typeConsole= Console.type
Becausethisprogramissmall,itmightbe
temptingtoassumethatitistrivialto
understand.Inthatrespect,Ifounditssize
tobedeceptive.Seethenextslideforthe
dependenciesthatexistbetweenits
modules.

@main defgo(): Unit= …
objectPrint {…}
typePrint= …
typeConsole= …
objectConsole
extendsAnsicolor {…}
extension[A](print: Print[A]){…}
0
1
2
Tounderstandhowtheprogramworks,letusconsidereachcomponentinturn,
intheorderimposedbythenumericlabelsshownnexttothecomponents.
3
4
5

objectConsole extendsAnsicolor {…}0
ThefirstcomponenttoconsiderisConsole.Allweneedtosay
aboutit,isthatitisasingletonobjectprovidingtwomethods
thatareusedbytheprogramtowritetotheconsole.
/** Prints an object to `out` using its `toString` method.
*
* @param obj the object to print; may be null.
* @group console-output
*/
defprint(obj: Any): Unit= …
/** Prints out an object to the default output, followed
* by a newline character.
*
* @param x the object to print.
* @group console-output
*/
defprintln(x: Any): Unit= …

Thenextcomponenttoconsideristhefollowingtypealiasdefinition.
WhatisConsole.type,andwhyisConsoleneeded?
Console.typeisthesingletontypeofsingletonobjectConsole.
Ifyoualreadyknewthat,youcanskipthenexttwoslides,whichprovideabitmoredetailonthattopic.
AstothereasonfordefiningConsole,itisconvenience.
SincetheprogramcontainsreferencestothetypeofConsole,itislessverboseandlessdistractingto
referencethetypewithConsolethanwithConsole.type.
// For convenience, so we don't have
// to write Console.type everywhere.
typeConsole= Console.type
1

Evaluation semantics
Scala’s singleton objects are instantiated lazily. To illustrate that, let’s print something to the standard output in the body of our Colors object:
object Colors {
println("initialising common colors")
val White: Color = new Color(255, 255, 255)
val Black: Color = new Color(0, 0, 0)
val Red: Color = new Color(255, 0, 0)
val Green: Color = new Color(0, 255, 0)
val Blue: Color = new Color(0, 0, 255)
}
If you re-start the REPL, you won’t see anything printed to the standard output just yet.
As soon as you access the Colors object for the first time, though, it will be initialised
and you will see something printed to the standard output.
Accessing the object after that will not print anything again, because the object has
already been initialised. Let’s try this out in the REPL:
scala> val colors = Colors
initialising common colors
val colors: Colors.type = Colors$@4e060c41
scala> val blue = colors.Blue
val blue: Color = Color@61da01e6
This lazy initialization is pretty similar to how the singleton pattern is often implemented in Java.
Daniel Westheide
@kaffeecoder

The type of a singleton object
As we have seen in the REPL output above, the type of the expression Colorsis not Colors,
but Colors.type, and the toString value in that REPL session was Colors$@4e060c41.
That last part is the object id and will be a different one every time you start a new Scala REPL.
What can we learn from this? For one, Colorsitself is not a type, or a class, it is an instance of a type.
Also, while there can be an arbitrary number of values of type Meeple, there can only be exactly one
value of type Colors.type, and that value is bound to the name Colors.
Because of this, Colors.typeis called a singleton type.
Since Colorsitself is not a type, you cannot define a function with the following signature:
def pickColor(colors: Colors): Color
You can define a function with this signature, though:
def pickColor(colors: Colors.type): Color Daniel Westheide
@kaffeecoder

Thenextcomponenttolookatisthefollowingtypealiasdefinition:
OnthelefthandsideofthedefinitionwehavePrint[A],whichisadescriptionofaneffect.
Butwhatdoestherighthandsideofthedefinitionmean?
ItisthetypeofaContextFunction.
typePrint[A] = Console?=> A2
Noel Welsh
@noelwelsh
APrint[A]isadescription:aprogramthatwhenrunmayprinttotheconsoleandalso
computeavalueoftypeA.Itisimplementedasacontextfunction.
Youcanthinkofacontextfunctionasanormalfunctionwithgiven(implicit)parameters.
InourcaseaPrint[A]isacontextfunctionwithaConsolegivenparameter(Consoleisa
typeintheScalastandardlibrary.)
AdamWarskiwroteagreatblogpostonContextFunctionscalledContextisKing.
TofurtherunderstandwhataContextFunctionis,let’slookatsomeexcerptsfromthatpost.

Whatisacontextfunction?
Beforewediveintousageexamplesandconsiderwhyyouwouldbeatallinterestedinusingcontextfunctions,let’sseewhattheyareand
howtousethem.
AregularfunctioncanbewritteninScalainthefollowingway:
val f: Int => String = (x: Int) => s"Got: $x"
Acontextfunctionlookssimilar,however,thecrucialdifferenceisthattheparametersareimplicit.Thatis,whenusingthefunction,the
parametersneedtobeintheimplicitscope,andprovidedearliertothecompilere.g.usinggiven;bydefault,theyarenotpassedexplicitly.
Thetypeofacontextfunctioniswrittendownusing?=>insteadof=>,andintheimplementation,wecanrefertotheimplicitparameters
thatareinscope,asdefinedbythetype.InScala3,thisisdoneusingsummon[T],whichinScala2hasbeenknownasimplicitly[T].Here,
inthebodyofthefunction,wecanaccessthegivenIntvalue:
val g: Int ?=> String = s"Got: ${summon[Int]}"
JustasfhasatypeFunction1,gisaninstanceofContextFunction1:
val ff: Function1[Int, String] = f
val gg: ContextFunction1[Int, String] = g
Contextfunctionsareregularvaluesthatcanbepassedaroundasparametersorstoredincollections.
Adam Warski
@adamwarski
Context is King
https://blog.softwaremill.com/context-is-king-20f533474cb3

Adam Warski
@adamwarski
Context is King
Wecaninvokeacontextfunctionbyexplicitlyprovidingtheimplicitparameters:
println(g(using 16))
Orwecanprovidethevalueintheimplicitscope.Thecompilerwillfigureouttherest:
println {
given Int = 42
g
}
Sidenote: you should never use “common” types such asIntorStringfor given/implicit values. Instead, anything that ends up in the implicit
scope should have a narrow, custom type, to avoid accidental implicit scope contamination.
val g: Int ?=> String = s"Got: ${summon[Int]}"

Inthenextexcerpt,Adamlooksatan
exampleusageofcontextfunctions.

Adam Warski
@adamwarski
Context is King
Sane ExecutionContexts
Let’sstartlookingatsomeusages!Ifyou’vebeendoinganyprogrammingusingScala2andAkka,you’veprobablyencountered
theExecutionContext.AlmostanymethodthatwasdealingwithFuturesprobablyhadtheadditionalimplicitec:ExecutionContextparameter
list.
Forexample,here’swhatasimplifiedfragmentofabusinesslogicfunctionthatsavesanewusertothedatabase,ifauserwiththegivenemail
doesnotyetexist,mightlooklikeinScala2:
case class User(email: String)
def newUser(u: User)(implicit ec: ExecutionContext): Future[Boolean] = {
lookupUser(u.email).flatMap {
case Some(_) => Future.successful(false)
case None => saveUser(u).map(_ => true)
}
}
def lookupUser(email: String)(implicit ec: ExecutionContext): Future[Option[User]] = ???
def saveUser(u: User)(implicit ec: ExecutionContext): Future[Unit] = ???
WeassumethatthelookupUserandsaveUsermethodsinteractwiththedatabaseinsomeasynchronousorsynchronousway.
NotehowtheExecutionContextneedstobethreadedthroughalloftheinvocations.It’snotadeal-breaker,butstillanannoyanceandone
morepieceofboilerplate.
ItwouldbegreatifwecouldcapturethefactthatwerequiretheExecutionContextinsomeabstractway…

Turns out, with Scala 3we can! That’s what context functionsare for. Let’s define a type alias:
typeExecutable[T] = ExecutionContext?=> Future[T]
AnymethodwheretheresulttypeisanExecutable[T],willrequireagiven(implicit)executioncontexttoobtaintheresult(theFuture).
Here’swhatourcodemightlooklikeafterrefactoring:
case class User(email: String)
def newUser(u: User): Executable[Boolean] = {
lookupUser(u.email).flatMap {
case Some(_) => Future.successful(false)
case None => saveUser(u).map(_ => true)
}
}
def lookupUser(email: String): Executable[Option[User]] = ???
def saveUser(u: User): Executable[Unit] = ???
The type signatures are shorter —that’s one gain. The code is otherwise unchanged —that’s another gain.
For example, thelookupUsermethod requires anExecutionContext. It is automatically provided by the compiler since it is in scope —as
specified by the top-level context functionmethod signature.
Adam Warski
@adamwarski
Context is King

case class User(email: String)
def newUser(u: User)(implicitec: ExecutionContext): Future[Boolean] = {
lookupUser(u.email).flatMap {
case Some(_) => Future.successful(false)
case None => saveUser(u).map(_ => true)
}
}
def lookupUser(email: String)(implicitec: ExecutionContext): Future[Option[User]] = ???
def saveUser(u: User)(implicitec: ExecutionContext): Future[Unit] = ???
typeExecutable[T] = ExecutionContext?=> Future[T]
case class User(email: String)
def newUser(u: User): Executable[Boolean] = {
lookupUser(u.email).flatMap {
case Some(_) => Future.successful(false)
case None => saveUser(u).map(_ => true)
}
}
def lookupUser(email: String): Executable[Option[User]] = ???
def saveUser(u: User): Executable[Unit] = ???
Thisslideandthenextoneare
justarecapofthechangesthat
Adammadewhenheintroduced
contextfunctions.

ThenextcomponenttolookatissingletonobjectPrint.
Theprintandprintlnfunctionsarestraightforward:givenamessageandanimplicitconsole,they
displaythemessagebypassingittothecorrespondingconsolefunctions.
Therunfunctionisalsostraightforward:givenaPrint[A],i.e.avalue(contextfunction)describinga
printingeffect(afunctionaleffect),itcarriesout(runs)theeffect,whichcausestheprinting(theside
effect)totakeplace(togetdone).ThewayitdoesthisisbymakingavailableanimplicitConsole,and
returningPrint[A],whichcausesthelatter(i.e.acontextfunction)tobeinvoked.
3objectPrint{
defprint(msg: Any)(usingc: Console): Unit=
c.print(msg)
defprintln(msg: Any)(usingc: Console): Unit=
c.println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for `Print`values */
…<we’ll come back to this later>…
}
typePrint[A] = Console?=> A

Thenextcomponentweneedtolookatisthemainfunction:
Holdonasecond!WesawonthepreviousslidethatPrint.println
returnsUnit,sohowcanthetypeofmessagebePrint[Unit]?
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
Noel’sexplanationcanbefoundonthenextslide.
@main defgo(): Unit= {
// Declare some `Prints`
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
// Composition
//…<we’ll come back to this later>…
// Make some output
Print.run(message)
//…<we’ll come back to this later>…
}
4

Noel Welsh
@noelwelsh
Context function typeshave a special rulethat makes constructing them easier: a normal expression will be
converted to an expression that produces a context functionif the type of the expression is a context function.
Let’s unpack that by seeing how it works in practice. In the example above we have the line
val message: Print[Unit] =
Print.println("Hello from direct-style land!")
Print.printlnis an expression with typeUnit, not a context function type.
However Print[Unit]is a context function type. This type annotation causes Print.println to be converted
to a context function type.
You can check this yourself by removing the type annotation:
val message =
Print.println("Hello from direct-style land!")
This will not compile.

objectPrint{
defprint(msg: Any)(usingc: Console): Unit=
c.print(msg)
defprintln(msg: Any)(usingc: Console): Unit=
c.println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for `Print`values */
…<we’ll come back to this later>…
}
objectPrint{
defprint(msg: Any): Print[Unit]=
summon[Console].print(msg)
defprintln(msg: Any): Print[Unit]=
summon[Console].println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for `Print`values */
…<we’ll come back to this later>…
}
FWIW,lookingbackatPrint,Icouldn’thelptryingoutthefollowingsuccessful
modification,whichchangestheprintandprintlnfunctionsfromside-effecting,i.e.
invokingthemcausessideeffects,toeffectful,i.e.theyreturnafunctionaleffect,a
descriptionofacomputationwhich,whenexecuted,willcausesideeffects.
Notethatinthefollowing,theabovemodificationdoesnotchangetheneedformessagetobeannotatedwithtypePrint[Unit]
Myexplanationisthatwithorwithoutthemodification,expressionPrint.println("Hello from direct-style land!")cannotbeevaluatedwithout
aconsolebeingavailable,butnoneareavailable,somessagecannotbeoftypeUnit.Withoutthemodification,theexpressionisautomaticallyconvertedtoa
contextfunction,whereaswiththemodification,thevalueoftheexpressionisalreadyacontextfunction.Inbothcases,thecontextfunctioncannotbe
invoked(duetonoconsolebeingavailable),somessagehastobeassignedthecontextfunction.
val message: Print[Unit] =
Print.println("Hello from direct-style land!")

@main defgo(): Unit= {
// Declare some `Prints`
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
// Composition
//…<we’ll come back to this later>…
// Make some output
Print.run(message)
//…<we’ll come back to this later>…
}
objectPrint{
defprint(msg: Any)(usingc: Console): Unit=
c.print(msg)
defprintln(msg: Any)(usingc: Console): Unit=
c.println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for`Print` values */
…<we’ll come back to this later>…
}typePrint[A] = Console?=> A
// For convenience, so we don't have
// to write Console.type everywhere.
typeConsole= Console.type
Let’srunthecodethatwehaveseensofar:
It works!
$ sbt run
[info] welcome to sbt 1.9.9 (Eclipse Adoptium Java 17.0.7)

[info] running go
Hello from direct-style land!
[success] Total time: 1 s, completed 5 May 2024, 17:15:32

Noel Welsh
@noelwelsh

Imaginewewanttocomposetheeffectofprintingtotheconsolewiththeeffectthatchangesthecolorof
thetextontheconsole.Withtheprintlnsideeffectwecannotdothis.Oncewecallprintlntheoutputis
alreadyprinted;thereisnoopportunitytochangethecolor.

Whatwereallywantistowritecodelike
Effect.println("OMG, it's an effect").foregroundBrightRed
whichlimitstheforegroundcolourtojustthegiventext.Wecanonlydothatifwehaveaseparationbetween
describingtheeffect,aswehavedoneabove,andactuallyrunningit.
Rememberthis?
@main defgo(): Unit= {
// Declare some `Prints`
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
// Composition
valred: Print[Unit] =
Print.println("Amazing!").prefix(Print.print("> ").red)
// Make some output
Print.run(message)
Print.run(red)
}
Thefinalcomponentthatneedslookingat(seenext
slide)supportsexactlytheaboveapproachto
composingeffects.
Hereontheright(highlightedinyellow)isanexample
ofitsusage(viewingthiscodehasbeenpostponeduntil
now).
Theredeffectcomposestheeffectofprinting”>”with
theeffectofchangingthestring’scolortored,andthen
composestheresultingeffectwiththeeffectof
printing“Amazing!”,whichisdonebyprefixingthe
stringprintedinredbytheformereffect,withthe
stringprintedbythelattereffect.

Noel Welsh
@noelwelsh
extension[A](print: Print[A]) {
/** Insert a prefix before `print` */
defprefix(first: Print[Unit]): Print[A] =
Print{
first
print
}
/** Use red foreground color when printing */
def red: Print[A] =
Print {
Print.print(Console.RED)
val result = print
Print.print(Console.RESET)
result
}
}
5@main defgo(): Unit= {
// Declare some `Prints`
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
// Composition
valred: Print[Unit] =
Print.println("Amazing!").prefix(Print.print("> ").red)
// Make some output
Print.run(message)
Print.run(red)
}
objectPrint{
defprint(msg: Any)(usingc: Console): Unit=
c.print(msg)
defprintln(msg: Any)(usingc: Console): Unit=
c.println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for `Print` values */
inline def apply[A](inline body: Console ?=> A): Print[A] =
body
}
Running a Print[A] uses another bit of special sauce:
if there is a given value of the correct type in scope of a
context function, that given value will be automatically
applied to the function. This is also what makes direct-
style composition, an example of which is shown above,
work. The calls toPrint.printare in a context where
a Console is available, and so will be evaluated once
the surrounding context function is run.
We use the same trick (see green box) with Print.apply, which is a general purpose constructor. You can callapplywith any expression and it will be converted
to a context function. (As far as I know it is not essential to useinline, but all the examples I learned from do this so I do it as well. I assume it is an optimization.)
Context function types have a
special rule that makes
constructing them easier: a normal
expression will be converted to an
expression that produces a
context function if the type of the
expression is a context function.
Here (on the left)
you can see how
functions prefix
and red(used on
the right) are
implemented

extension[A](print: Print[A]) {
/** Insert a prefix before `print` */
defprefix(first: Print[Unit]): Print[A] =
Print{
first(ev$0)
print(ev$0)
}(ev$0)
/** Use red foreground color when printing */
def red: Print[A] =
Print {
Print.print(Console.RED)(ev$0)
val result: A = print(ev$0)
Print.print(Console.RESET)(ev$0)
result
}(ev$0)
}
5Tohelpunderstandtheextensionfunctionsfor
composingeffects,hereistheircodeagainbut
withIntelliJIDEA’sX-RayModeswitchedon.
ev$0standsforevidence,anditstypeisConsole.

RememberhowAdammodifiedhiscodetousecontextfunctions?Seethenextslide
foranimportantpointthathemadeinhisblogpostafterreflectingonthosechanges.

Adam Warski
@adamwarski
Context is King
Executableasanabstraction
However,thepurelysyntacticchangewe’veseenabove—givinguscleanertypesignatures—isn’ttheonlydifference.Sincewenowhaveanabstractionfor“a
computationrequiringanexecutioncontext”,wecanbuildcombinatorsthatoperateonthem.Forexample:
// retries the given computation up to `n` times, and returns the
// successful result, if any
def retry[T](n: Int, f: Executable[T]): Executable[T]
// runs all of the given computations, with at most `n` running in
// parallel at any time
def runParN[T](n: Int, fs: List[Executable[T]]): Executable[List[T]]
Thisispossiblebecauseofaseeminglyinnocentsyntactic,buthugesemanticaldifference.Theresultofamethod:
def newUser(u: User)(implicit ec: ExecutionContext): Future[Boolean]
isarunningcomputation,whichwilleventuallyreturnaboolean.Ontheotherhand:
def newUser(u: User): Executable[Boolean]
returnsalazycomputation,whichwillonlyberunwhenanExecutionContextisprovided(eitherthroughtheimplicitscopeorexplicitly).Thismakesitpossibleto
implementoperatorsasdescribedabove,whichcangovernwhenandhowthecomputationsarerun.
Ifyou’veencounteredtheIO,ZIOorTaskdatatypesbefore,thismightlookfamiliar.Thebasicideabehindthosedatatypesissimilar:captureasynchronous
computationsaslazilyevaluatedvalues,andprovidearichsetofcombinators,formingaconcurrencytoolkit.Takealookatcats-effect,Monix,orZIOformore
details!
type Executable[T] = ExecutionContext ?=> Future[T]

// retries the given computation up to `n` times, and returns the
// successful result, if any
def retry[T](n: Int, f: Executable[T]): Executable[T] = …
// runs all of the given computations, with at most `n` running in
// parallel at any time
def runParN[T](n: Int, fs: List[Executable[T]]): Executable[List[T]] = …
extension[A](print: Print[A]) {
/** Insert a prefix before `print` */
defprefix(first: Print[Unit]): Print[A] = …
/** Use red foreground color when printing */
def red: Print[A] = …
}
type Executable[T] = ExecutionContext ?=> Future[T] typePrint[A] = Console?=> A
Thisispossiblebecauseofaseeminglyinnocentsyntactic,buthugesemanticaldifference.The
resultofamethod:
def newUser(u: User)(implicit ec: ExecutionContext): Future[Boolean]
isarunningcomputation,whichwilleventuallyreturnaboolean.Ontheotherhand:
def newUser(u: User): Executable[Boolean]
returnsalazycomputation,whichwillonlyberunwhenanExecutionContextisprovided(either
throughtheimplicitscopeorexplicitly).Thismakesitpossibletoimplementoperatorsas
describedabove,whichcangovernwhenandhowthecomputationsarerun.
Thisispossiblebecauseofaseeminglyinnocentsyntactic,buthugesemantical
difference.Theresultofamethod:
defprint(msg:Any)(using c: Console): Unit
isaprintingsideeffect.Ontheotherhand:
def print(msg: Any): Print[Unit]
returnsalazycomputation,whichwillonlyberunwhenaConsoleisprovided(either
throughthegivenscopeorexplicitly).Thismakesitpossibletoimplementoperators
asdescribedabove,whichcangovernhowthecomputationsarecomposed.
IamseeingthefollowingsimilaritywiththePrint[A]program
While it is quite possible to increase the similarity by changing Print[Unit] toPrint[A] or Print[B], it makes sense not to do so because the prefix function discards the value of its parameter.

Let’suncommentthelastfewbitsofcodeandruntheprogramagain:
And yes, we now see two messages rather than one, the second one being printed by the composite effect.
As a recap, in the next slide we see the whole program.
$ sbt run
[info] welcome to sbt 1.9.9 (Eclipse Adoptium Java 17.0.7)

[info] running go
Hello from direct-style land!
>Amazing!
[success] Total time: 1 s, completed 5 May 2024, 17:15:32

Noel Welsh
@noelwelsh
@main defgo(): Unit= {
// Declare some `Prints`
valmessage: Print[Unit] =
Print.println("Hello from direct-style land!")
// Composition
valred: Print[Unit] =
Print.println("Amazing!").prefix(Print.print("> ").red)
// Make some output
Print.run(message)
Print.run(red)
}
objectPrint{
defprint(msg: Any)(usingc: Console): Unit=
c.print(msg)
defprintln(msg: Any)(usingc: Console): Unit=
c.println(msg)
defrun[A](print: Print[A]): A = {
givenc: Console= Console
print
}
/** Constructor for `Print`values */
inlinedefapply[A](inlinebody: Console?=> A): Print[A] =
body
}
extension[A](print: Print[A]) {
/** Insert a prefix before `print`*/
defprefix(first: Print[Unit]): Print[A] =
Print{
first
print
}
/** Use red foreground color when printing */
defred: Print[A] =
Print{
Print.print(Console.RED)
valresult = print
Print.print(Console.RESET)
result
}
}
typePrint[A] = Console?=> A
// For convenience, so we don't have
// to write Console.type everywhere.
typeConsole= Console.type

ToconcludePart1,inthenextandslide,Noel
describestheconceptsbehindhisprogram.

Noel Welsh
@noelwelsh
That’sthemechanicsofhowdirect-styleeffectsystemsworkinScala:itallcomesdowntocontextfunctions.
Noticewhatwehaveintheseexamples:wewritecodeinthenaturaldirectstyle,butwestillhaveaninformative
type,Print[A],thathelpsusreasonabouteffectsandwecancomposetogethervaluesoftypePrint[A].
I’mgoingtodealwithcompositionofdifferenteffectsandmoreinjustabit.Firstthough,Iwantdescribetheconceptsbehind
whatwe’vedone.
Noticeindirect-styleeffectswespliteffectsintotwoparts:contextfunctionsthatdefinetheeffectsweneed,andtheactual
implementationofthoseeffects.Intheliteraturethesearecalledalgebraiceffectsandeffecthandlersrespectively.Thisisan
importantdifferencefromIO,wherethesametypeindicatestheneedforeffectsandprovidestheimplementationofthose
effects.
Alsonoticethatweusetheargumenttypeofcontextfunctionstoindicatetheeffectsweneed,rathertheresulttypeasin
monadiceffects.Thisdifferenceavoidsthe“coloredfunction”problemwithmonads.Wecanthinkoftheargumentsas
specifyingrequirementsontheenvironmentorcontextinwhichthecontextfunctions[operate?],hencethename.
Nowlet’slookatcompositionofeffects,andeffectsthatmodifycontrolflow.

That’sitforPart1.Ifyoulikedit,considercheckingout
https://fpilluminated.com/ formorecontentlikethis.
Tags