Skip to content

Commit

Permalink
feat: add section effect-oriented programming (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
magnus-madsen authored Oct 16, 2024
1 parent a739184 commit 8e011d4
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 150 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [Effect Polymorphism](./effect-polymorphism.md)
- [Effects and Handlers](./effects-and-handlers.md)
- [Library Effects](./library-effects.md)
- [Effect-Oriented Programming](./effect-oriented-programming.md)
- [Modules](./modules.md)
- [Declaring Modules](./declaring-modules.md)
- [Using Modules](./using-modules.md)
Expand Down
157 changes: 157 additions & 0 deletions src/effect-oriented-programming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
## Effect-Oriented Programming

Programming with effects requires a new mindset, _an effect-oriented mindset_.

Imagine a programmer coming from JavaScript or Python to a statically-typed
programming language such as C# or Java. If they continue to program with
objects, maps, and strings without introducing their own types, then the
benefits of a static type system are lost. In the same way, if a programmer
comes to Flix without adapting _an effect-oriented mindset_ then the benefits of
the Flix type and effect system are lost.

In Flix, we can give every function the `IO` effect and call effectful code
everywhere, but this is not effect-oriented programming and is a bad programming
style. A proper effect-oriented program architecture consists of a functional
core, which may use [algebraic effects and handlers](./effects-and-handlers.md),
surrounded by an imperative shell that performs `IO`. A good rule of thumb is
that `IO` effect should be _close_ to the `main` function.

We now illustrate these points with an example.

#### A Guessing Game — The Wrong Way

Consider the following program written in a mixed style of Flix and Java:

```flix
import java.lang.System
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.{Random => JRandom}
def getSecretNumber(): Int32 \ IO =
let rnd = new JRandom();
rnd.nextInt()
def readGuess(): Result[String, String] \ IO =
let reader = new BufferedReader(new InputStreamReader(System.in));
let line = reader.readLine();
if (Object.isNull(line))
Result.Err("no input")
else
Result.Ok(line)
def readAndParseGuess(): Result[String, Int32] \ IO =
forM(g <- readGuess();
n <- Int32.parse(10, g)
) yield n
def gameLoop(secret: Int32): Unit \ IO = {
println("Enter a guess:");
match readAndParseGuess() {
case Result.Ok(g) =>
if (secret == g) {
println("Correct!")
} else {
println("Incorrect!");
gameLoop(secret)
}
case Result.Err(_) =>
println("Not a number? Goodbye.");
println("The secret was: ${secret}")
}
}
def main(): Unit \ IO =
let secret = getSecretNumber();
gameLoop(secret)
```

Here every function, i.e. `getSecretNumber`, `readGuess`, `readAndParseGuess`,
`gameLoop`, and `main` has the `IO` effect. The consequence is that every
function can do anything. Note how effectful code is scattered everywhere
throughout the program.

Understanding, refactoring, and testing a program written in this style is a
nightmare.

Programming in a effect-oriented style means that we should define effects for
every action that interacts with the outside world. We should then _handle_
these effects close to the `main` function.

#### A Guessing Game &mdash; The Right Way

Here is what we should have done:

```flix
import java.lang.System
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.{Random => JRandom}
eff Guess {
pub def readGuess(): Result[String, String]
}
eff Secret {
pub def getSecret(): Int32
}
eff Terminal {
pub def println(s: String): Unit
}
def readAndParseGuess(): Result[String, Int32] \ {Guess} =
forM(g <- do Guess.readGuess();
n <- Int32.parse(10, g)
) yield n
def gameLoop(secret: Int32): Unit \ {Guess, Terminal} = {
do Terminal.println("Enter a guess:");
match readAndParseGuess() {
case Result.Ok(g) =>
if (secret == g) {
do Terminal.println("Correct!")
} else {
do Terminal.println("Incorrect!");
gameLoop(secret)
}
case Result.Err(_) =>
do Terminal.println("Not a number? Goodbye.");
do Terminal.println("The secret was: ${secret}")
}
}
def main(): Unit \ IO =
try {
let secret = do Secret.getSecret();
gameLoop(secret)
} with Secret {
def getSecret(_, resume) =
let rnd = new JRandom();
resume(rnd.nextInt())
} with Guess {
def readGuess(_, resume) =
let reader = new BufferedReader(new InputStreamReader(System.in));
let line = reader.readLine();
if (Object.isNull(line))
resume(Result.Err("no input"))
else
resume(Result.Ok(line))
} with Terminal {
def println(s, resume) = { println(s); resume() }
}
```

Here, we have introduced three algebraic effects:

1. A `Guess` effect that represents the action of asking the user for a guess.
2. A `Secret` effect that represents the action of picking a secret number.
3. A `Terminal` effect that represents the action of printing to the console.

We have written each function to only use the relevant effects. For example, the
`gameLoop` function uses the `Guess` and `Terminal` effects &mdash; and has no
other effects. Furthermore, all effects are now handled in one place: in the
`main` function. The upshot is that the business is logic is purely functional.
Where impurity is needed, it is precisely encapsulated by the use of effects and
handlers.
150 changes: 0 additions & 150 deletions src/foundational-effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,153 +98,3 @@ constructors, and methods in the Java Class Library to foundational effects. For
example, the database assigns the `Exec` effects to every constructor and method
in the `java.lang.Process`, `java.lang.ProcessBuilder`, and
`java.lang.ProcessHandle` classes.

### How to Program with Foundational Effects

In Flix, we can call Java code when and wherever we want, but it is considered
bad programming style. A proper program architecture consists of a functional
core, which may use [algebraic effects and handlers](./effects-and-handlers.md),
surrounded by an imperative shell that may call into Java. Such a design is
modular, debuggable, and testable.

Simply put code with foundational effects should be _close_ to the `main`
function.

#### A Guessing Game &mdash; The Wrong Way

Consider the following program written in a mixed style of Flix and Java:

```flix
import java.lang.System
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.{Random => JRandom}
def getSecretNumber(): Int32 \ IO =
let rnd = new JRandom();
rnd.nextInt()
def readGuess(): Result[String, String] \ IO =
let reader = new BufferedReader(new InputStreamReader(System.in));
let line = reader.readLine();
if (Object.isNull(line))
Result.Err("no input")
else
Result.Ok(line)
def readAndParseGuess(): Result[String, Int32] \ IO =
forM(g <- readGuess();
n <- Int32.parse(10, g)
) yield n
def gameLoop(secret: Int32): Unit \ IO = {
println("Enter a guess:");
match readAndParseGuess() {
case Result.Ok(g) =>
if (secret == g) {
println("Correct!")
} else {
println("Incorrect!");
gameLoop(secret)
}
case Result.Err(_) =>
println("Not a number? Goodbye.");
println("The secret was: ${secret}")
}
}
def main(): Unit \ IO =
let secret = getSecretNumber();
gameLoop(secret)
```

The problem is that every function: `getSecretNumber`, `readGuess`,
`readAndParseGuess`, `gameLoop`, and `main` has the `IO` effect. The consequence
is that every function can do anything. Note how the effectful code responsible
for interoperability with Java is scattered all over the program. Understanding,
refactoring, and testing a program written in this style is a nightmare.

Programming in a style where every function has the `IO` effect is akin to
programming in a style where every function argument and return type has the
`Object` type &mdash; it makes the type (or effect) system useless.

Programming in a programming language with algebraic effects means that we
should define effects for each action or interaction with the outside world. We
should then _handle_ these effects close to the program's boundary.

#### A Guessing Game &mdash; The Right Way

Here is what we should have done:

```flix
import java.lang.System
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.{Random => JRandom}
eff Guess {
pub def readGuess(): Result[String, String]
}
eff Secret {
pub def getSecret(): Int32
}
eff Terminal {
pub def println(s: String): Unit
}
def readAndParseGuess(): Result[String, Int32] \ {Guess} =
forM(g <- do Guess.readGuess();
n <- Int32.parse(10, g)
) yield n
def gameLoop(secret: Int32): Unit \ {Guess, Terminal} = {
do Terminal.println("Enter a guess:");
match readAndParseGuess() {
case Result.Ok(g) =>
if (secret == g) {
do Terminal.println("Correct!")
} else {
do Terminal.println("Incorrect!");
gameLoop(secret)
}
case Result.Err(_) =>
do Terminal.println("Not a number? Goodbye.");
do Terminal.println("The secret was: ${secret}")
}
}
def main(): Unit \ IO =
try {
let secret = do Secret.getSecret();
gameLoop(secret)
} with Secret {
def getSecret(_, resume) =
let rnd = new JRandom();
resume(rnd.nextInt())
} with Guess {
def readGuess(_, resume) =
let reader = new BufferedReader(new InputStreamReader(System.in));
let line = reader.readLine();
if (Object.isNull(line))
resume(Result.Err("no input"))
else
resume(Result.Ok(line))
} with Terminal {
def println(s, resume) = { println(s); resume() }
}
```

Here, we have introduced three effects:

1. An effect `Secret` that represents the action of picking a secret number.
2. An effect `Guess` that represents the action of asking the user for a guess.
3. An effect `Terminal` that represents the action of printing to the console.

We have written each function to use the relevant effects. For example, the
`gameLoop` function uses the `Guess` and `Terminal` effects &mdash; and has no
other effects. Moreover, all effects are now handled in one place: in the `main`
function. The upshot is that the program's core does not have to worry about
interoperability.

0 comments on commit 8e011d4

Please sign in to comment.