Skip to content

Commit

Permalink
Changed introduction
Browse files Browse the repository at this point in the history
Added explanation about computation
Changed paragraphs
Changed the referential transparency example
  • Loading branch information
Hombre-x committed Aug 18, 2024
1 parent 9bf9205 commit 68600f5
Showing 1 changed file with 61 additions and 23 deletions.
84 changes: 61 additions & 23 deletions docs/introduction/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

In shellfish, you have two different ways of doing things:

- First, if you prefer a more concise, fluent syntax, use the extension methods (e.g. `path.read()`) by importing the `shellfish.syntax.all.*` package.
- First, if you prefer a more concise syntax, use the extension methods (e.g. `path.read()`) by importing the `shellfish.syntax.path.*` package.

- If you prefer calling static methods, use direct method calls on the `FilesOs` object (e.g., `FilesOs.read(path)`). You can also import all the functions inside the `shellfish.FilesOs` package if you don't want to call the `FilesOs` object every time (e.g., `read(path)`).

For this documentation we'll call the methods on the `FilesOs` objects in the static variant to difference them from the extension methods.
In this documentation we'll call the methods on the `FilesOs` objects in the static variant to differentiate them from the extension ones.

@:select(api-style)

Expand Down Expand Up @@ -40,18 +40,36 @@ yield ()

@:@

Which one you should use really depends on your preferences and choices; if you like the method style of calling functions directly to the object, stick to the extension methods syntax, if you rather prefer a more Haskell-like style, stick to the static object calls!
Which one you should use really depends on your preferences and choices; if you like the method style of calling functions directly on the objects, stick to the extension methods syntax, if you rather prefer a more Haskell-like style, stick to the static object calls!

## Imports

If you just want to start scripting right away, importing `shellfish.*` will do the trick; it imports all the extension methods and functionality you need, such as types and functions, to start working right away.

But if you want more concise functionality, the `shellfish.syntax.path.*` and `shellfish.syntax.process.*` will only import the extension methods.
But if you want more concise functionality, the `shellfish.syntax.path.*` will only import the extension methods.

For the static methods, the `shellfish.FilesOs` and `shellfish.ProcessesOs` imports will provide the functions to work with files and processes, if that is your preferred style.
For the static methods, the `shellfish.FilesOs` will provide the functions to work with files, if that is your preferred style.

Finally, `shellfish.domain.*` will import all the types needed to work with the library!


## Talking about computations

Throughout this library you will often see the word calculation, but what is it? A computation is a well-defined, step-by-step work that calculates a value when evaluated (and can also perform side effects along the way). For example, this is a computation:

```scala
object ProgramOne:
val computationOne =
val a = 2
val b = 3
println(s"a: $a, b: $b")
a + 2 * b
```

In this case, the program `ProgramOne` has a computation that calculates 2 + 2 * 3 and logs some values to the console. When this computation is evaluated (for example, by a main function), it will compute the value 8.

This may seem trivial, but someone has to put the nail before hammering.

## What is this `IO` thing?

You may be wondering at this point, what is this `IO` thing that appears at the end of the functions? Well, that's the [IO monad](https://typelevel.org/cats-effect/docs/2.x/datatypes/io) of Cats Effect; the concept is much more extensive than we can explain on this page, but basically it is a type that allows us to suspend side effects so that they do not run instantly:
Expand All @@ -74,7 +92,7 @@ import cats.effect.unsafe.implicits.global // Imports the runtime that executes
suspendingHello.unsafeRunSync()
```

But this is not the usual way; the common way is to pass it to the `run` function (similar to the main method in scala, but for `IO`!). To that, you have to extend the object with `IOApp`:
But this is not the usual way: the usual way is to passing it to the `run` function (similar to the main method but for `IO`). To that, you have to extend your application's main object with `IOApp`:

```scala mdoc:silent
import cats.effect.IOApp
Expand All @@ -86,7 +104,7 @@ object Main extends IOApp.Simple:
end Main
```

On either way, the `IO` will be executed and all the computation will be evaluated.
Either way, the `IO` will be executed and all the computation will be evaluated.

But why's that useful? Well, one of the advantages is referential transparency, and that basically means that we can replace the code wherever it is referenced and expect the same results every time:

Expand All @@ -99,37 +117,57 @@ val num = 2
It may seem trivial, but that's not always the case:

```scala mdoc
import scala.util.Random
val salute =
println("Hellow, ")
"Hellow, "

val rndNum = Random.nextInt(10)
val meow =
println("meow.")
"meow"

val result1 = rndNum + rndNum
def result1: String = salute + meow
```

If referential transparency exists in your program, replacing `Random.nextInt(10)` in `rndNum` would yield the same result, which is not the case:
If referential transparency exists in your program, replacing `println("Hellow, "); "Hellow, "` in `salute` should fire the print to the console two times, same with `meow`, which is not the case:

```scala mdoc
val result2 = Random.nextInt(10) + Random.nextInt(10)
def result2: String = { println("Hellow, "); "Hellow, " } + { println("meow"); "meow" }

result1 == result2
result1
result2
```

With `IO`, we can solve this by delaying the number generation:
As you can see, only the `result1` printed twice, even though we replaced the exact same definitions with the implementations. With `IO`, we can solve this by delaying the print to the stout:

```scala mdoc:compile-only
import cats.syntax.all.*
```scala mdoc
val saluteIO = IO:
println("Hellow, ")
"Hellow, "

val rndNumIO = IO(Random.nextInt(10))
val meowIO = IO:
println("meow.")
"meow."

val result1IO = (rndNumIO, rndNumIO).mapN(_ + _)
def result1IO: IO[String] =
for
hello <- saluteIO
meow <- meowIO
yield hello + meow

val result2IO = (IO(Random.nextInt(10)), IO(Random.nextInt(10))).mapN(_ + _)
```
def result2IO: IO[String] =
for
hello <- IO { println("Hellow, "); "Hellow, " }
meow <- IO { println("meow"); "meow " }
yield hello + meow

For more examples of why this is important, check out [this blog post](https://blog.rockthejvm.com/referential-transparency/) about referential transparency!
result1IO.unsafeRunSync()
result2IO.unsafeRunSync()

```
Now both results are the same, an behave exactly the same!

Here's a [good explanation](https://blog.rockthejvm.com/referential-transparency/) about the benefits of referential transparency.

Another advantage is control over the code. Because "wrapping" computations inside the `IO` monad converts programs into descriptions of programs instead of statements, you automatically gain control over your program as to when and when not to execute those statements.

For those and many more reasons, we opted to make this library pure by implementing `IO` on the library!
Another benefit is gaining explicit control over code execution. By encapsulating computations within the `IO` monad, your programs become blueprints rather than direct statements. This gives you the ability to decide precisely when to execute those statements.

0 comments on commit 68600f5

Please sign in to comment.