First steps
Which API should I use?
In Catscript, you have two different ways of doing things:
-
First, if you prefer a more concise syntax, use the extension methods (e.g.
path.read()) by importing thecatscript.syntax.path.*package. -
If you prefer calling static methods, use direct method calls on the
Catscriptobject (e.g.,Catscript.read(path)). You can also import all the functions inside thecatscript.Catscriptpackage if you don't want to call theCatscriptobject every time (e.g.,read(path)).
In this documentation we'll call the methods on the Catscript objects in the static variant to differentiate them from the extension ones.
import catscript.syntax.all.*
val path = Path("data/test.txt")
for
file <- path.read
_ <- path.append("I'll place this here.")
yield ()
import catscript.Catscript
val path = Path("data/test.txt")
for
file <- Catscript.read(path)
_ <- Catscript.append(path, "I'll place this here.")
yield ()
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 catscript.* 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 catscript.syntax.path.* will only import the extension methods.
For the static methods, the catscript.Catscript will provide the functions to work with files, if that is your preferred style.
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:
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 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:
import cats.effect.IO
/* Will print to de console! */
val printingHello = println("Hello newbies!")
// Hello newbies!
/* Will not do anything (yet) */
val suspendingHello = IO(println("Hello newbies!"))
// suspendingHello: IO[Unit] = Delay(
// thunk = repl.MdocSession$MdocApp$$Lambda$13037/0x00007fc5ba038000@260d7711,
// event = cats.effect.tracing.TracingEvent$StackTrace
// )
To actually run the computation, you have two options, the first one (and not recommended) is to call the unsafeRunSync() function at the very end of the program:
import cats.effect.unsafe.implicits.global // Imports the runtime that executes the IO monad
suspendingHello.unsafeRunSync()
// Hello newbies!
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:
import cats.effect.IOApp
object Main extends IOApp.Simple:
def run: IO[Unit] = suspendingHello
end Main
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:
val num = 2
// num: Int = 2
(num + num) == (2 + 2)
// res1: Boolean = true
It may seem trivial, but that's not always the case:
val salute =
println("Hellow, ")
"Hellow, "
// Hellow,
// salute: String = "Hellow, "
val meow =
println("meow.")
"meow"
// meow.
// meow: String = "meow"
def result1: String = salute + meow
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:
def result2: String = { println("Hellow, "); "Hellow, " } + { println("meow"); "meow" }
result1
// res2: String = "Hellow, meow"
result2
// Hellow,
// meow
// res3: String = "Hellow, meow"
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:
val saluteIO = IO:
println("Hellow, ")
"Hellow, "
// saluteIO: IO[String] = Delay(
// thunk = repl.MdocSession$MdocApp$$Lambda$13097/0x00007fc5ba064cc0@7abe0ccc,
// event = cats.effect.tracing.TracingEvent$StackTrace
// )
val meowIO = IO:
println("meow.")
"meow."
// meowIO: IO[String] = Delay(
// thunk = repl.MdocSession$MdocApp$$Lambda$13098/0x00007fc5ba0651a0@2e1cfc8c,
// event = cats.effect.tracing.TracingEvent$StackTrace
// )
def result1IO: IO[String] =
for
hello <- saluteIO
meow <- meowIO
yield hello + meow
def result2IO: IO[String] =
for
hello <- IO { println("Hellow, "); "Hellow, " }
meow <- IO { println("meow"); "meow " }
yield hello + meow
result1IO.unsafeRunSync()
// Hellow,
// meow.
// res4: String = "Hellow, meow."
result2IO.unsafeRunSync()
// Hellow,
// meow
// res5: String = "Hellow, meow "
Now both results are the same, an behave exactly the same!
Here's a good explanation about the benefits of referential transparency.
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.
Weird >> and >>= operators, what are those?
While reading the documentation of this library, you may come across some strange operator like >>. This is convenient syntax sugar for some fairly common methods!
You can import them using the syntax package in cats, like this:
import cats.syntax.all.*
For instance, you may seen something like this:
IO("The result is: 42") >>= IO.println
That is just an alias for flatMap, so it's like writing IO("The result is: 42").flatMap(IO.println(_)), but without the added boilerplate. This use is more common in languages like Haskell, but we'll use it in the documentation to simplify things a bit!
The >> is even simpler:
IO.println("Loggin here... ") >> IO("Returning this string!")
This is used for concatenating monads that you do not care about the result of the computation, just like doing IO.println("Loggin here...").flatMap( _ => IO("Returning this string!")). When to use it? In the example from above, IO.println computes a Unit (), so you can't do much with it anyway, so the >> operator comes in handy!