From 68600f58c2e7ec28f97ba16043c9f6062b5dfab0 Mon Sep 17 00:00:00 2001 From: Hombre-x Date: Sun, 18 Aug 2024 17:46:25 -0500 Subject: [PATCH] Changed introduction Added explanation about computation Changed paragraphs Changed the referential transparency example --- docs/introduction/index.md | 84 +++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/docs/introduction/index.md b/docs/introduction/index.md index 2d0d89a..a589310 100644 --- a/docs/introduction/index.md +++ b/docs/introduction/index.md @@ -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) @@ -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: @@ -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 @@ -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: @@ -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.