Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ object LaikaBuild {
import pink.cozydev.protosearch.laika.IndexConfig
import pink.cozydev.protosearch.ui.SearchUI

val indexConfig = IndexConfig.default
// The interactive tutorials are an unlisted prototype; keep them out of
// search until they are ready to be announced.
val indexConfig =
IndexConfig.default.withExcludedPaths(Path.Root / "tutorials")

def input = {
val securityPolicy = new URI(
Expand Down
42 changes: 42 additions & 0 deletions src/tutorials/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{%
laika.title = Tutorials
%}

# Typelevel Tutorials

Welcome! These tutorials are hands-on: each lesson explains an idea on the left side
of the page and gives you a live Scala editor on the other. Edit the code, press
**Run**, and see what happens.

@:style(bulma-notification bulma-is-warning bulma-is-light)
This section is an early prototype. The lessons below exist to show that the
machinery works; their content is simply a few vibe-coded example pages. They
should be replaced by human-curated tracks that say what we want to say.
@:@

## Cats

[Cats](https://typelevel.org/cats/) is the foundation of the Typelevel
ecosystem: a library of type classes and data types for functional programming
in Scala.

@:navigationTree {
entries = [
{ target = "/tutorials/cats", excludeRoot = true, excludeSections = true }
]
}

## Cats Effect

[Cats Effect](https://typelevel.org/cats-effect/) builds on Cats to describe
side effects as values, and gives you a runtime to execute them.

@:navigationTree {
entries = [
{ target = "/tutorials/cats-effect", excludeRoot = true, excludeSections = true }
]
}

These tutorials were inspired by [scalatutorials](https://scalatutorials.com), which teaches

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you really want to recommend them, there is also that new learning resources page, which only has one link under tutorials and courses right now!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point -- would be totally appropriate to have there IMO. (Although probably in a separate PR, since it's not really related to this prototype.)

the Scala language itself. If you're just starting out, we recommend check that out first,
then come back here to help dive into Typelevel!
3 changes: 3 additions & 0 deletions src/tutorials/cats-effect/directory.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
laika.title = Cats Effect
laika.html.template = /tutorials/tutorial.template.html
laika.navigationOrder = [io.md, unsafe-run.md]
19 changes: 19 additions & 0 deletions src/tutorials/cats-effect/io.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{%
laika.title = IO
scastie.code = io.sc
%}

An `IO[A]` is a *description* of a computation that may perform side effects and
eventually produce an `A`. Building one runs nothing at all: `IO(println("hi"))`
is a value you can pass around, store in a list, or throw away.

That is the whole idea. Once effects are values, the ordinary tools work on
them: you can `map` and `flatMap` them into bigger programs, combine them in a
`for` comprehension, and repeat one simply by using it twice.

Notice in the editor that nothing is printed while the `IO`s are being built.
The description is inert until somebody runs it — which is the subject of the
next lesson.

**Try it:** reorder the steps in the `for` comprehension, or build an `IO` and
never use it.
19 changes: 19 additions & 0 deletions src/tutorials/cats-effect/io.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//> using dep org.typelevel::cats-effect:3.6.3

import cats.effect.IO

// building an IO performs no effect: this line prints nothing
val hello: IO[Unit] = IO(println("hello"))

// bigger programs are built out of smaller ones
val program: IO[Unit] =
for
_ <- hello
_ <- IO(println("and again"))
n <- IO(21 * 2)
_ <- IO(println(s"the answer is $n"))
yield ()

// still nothing has happened; `program` is only a description
println("built the program, and nothing has run yet")
println(program)
23 changes: 23 additions & 0 deletions src/tutorials/cats-effect/unsafe-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{%
laika.title = Running an IO
scastie.code = unsafe-run.sc
%}

Descriptions have to be executed eventually. In a real application that happens
once, at the edge of the world: you extend `IOApp`, hand it your program, and
Cats Effect runs it on its own runtime.

The `unsafeRun*` methods let you do it by hand. They are called *unsafe* not
because they are broken, but because they break the property that makes `IO`
worth having: they actually perform effects, and they can block or throw. Every
call is a place where functional reasoning stops.

- `unsafeRunSync()` blocks the current thread until the program finishes.
- `unsafeRunAndForget()` starts it and returns immediately.
- `unsafeToFuture()` hands you a `Future` of the result.

Running the same description twice performs the effects twice: an `IO` is a
recipe, not a cached result.

**Try it:** run `program` a second time, or comment out the `unsafeRunSync()`
call and watch the output disappear.
17 changes: 17 additions & 0 deletions src/tutorials/cats-effect/unsafe-run.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//> using dep org.typelevel::cats-effect:3.6.3

import cats.effect.IO
import cats.effect.unsafe.implicits.global

val program: IO[Int] =
for
_ <- IO(println("starting"))
n <- IO(21 * 2)
_ <- IO(println(s"finished with $n"))
yield n

// nothing above this line has printed anything
println("about to run")

val result = program.unsafeRunSync()
println(s"got back $result")
3 changes: 3 additions & 0 deletions src/tutorials/cats/directory.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
laika.title = Cats
laika.html.template = /tutorials/tutorial.template.html
laika.navigationOrder = [functor.md, monoid.md]
23 changes: 23 additions & 0 deletions src/tutorials/cats/functor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{%
laika.title = Functor
scastie.code = functor.sc
%}

A `Functor` is anything you can `map` over: a container or a context whose
contents can be transformed without changing the shape of the context itself.
`List`, `Option` and `Either` are all functors, and so are many types that don't
look like collections at all.

Cats captures this as a type class with a single abstract method:

```scala
trait Functor[F[_]]:
def map[A, B](fa: F[A])(f: A => B): F[B]
```

Because the type class is abstract in `F`, you can write code once and run it
against every functor. The `plusOne` method in the editor never mentions `List`
or `Option`, yet it works with both.

**Try it:** call `plusOne` with a `Vector`, or map an `Option` that is `None`
and see what comes back.
15 changes: 15 additions & 0 deletions src/tutorials/cats/functor.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//> using dep org.typelevel::cats-core:2.13.0

import cats.Functor
import cats.syntax.all.*

// `map` for any functor at all, chosen by the compiler from the type
def plusOne[F[_]: Functor](fa: F[Int]): F[Int] =
fa.map(_ + 1)

println(plusOne(List(1, 2, 3)))
println(plusOne(Option(41)))
println(plusOne(Option.empty[Int]))

// the type class instance is also available directly
println(Functor[List].map(List("cats", "effect"))(_.toUpperCase))
23 changes: 23 additions & 0 deletions src/tutorials/cats/monoid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{%
laika.title = Monoid
scastie.code = monoid.sc
%}

A `Monoid` is a type that knows how to combine two of its values, and what an
"empty" value looks like:

```scala
trait Monoid[A]:
def empty: A
def combine(x: A, y: A): A
```

Combining must be associative, and `empty` must not change anything it is
combined with. That is enough structure to fold a whole collection without
writing a fold: Cats gives you `combineAll` for free.

Numbers, strings, lists and maps are all monoids — and so is any tuple or map
whose contents are monoids, which is where it starts to pay off.

**Try it:** combine a `List[Map[String, Int]]` and watch the values under
duplicate keys get combined rather than overwritten.
17 changes: 17 additions & 0 deletions src/tutorials/cats/monoid.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//> using dep org.typelevel::cats-core:2.13.0

import cats.Monoid
import cats.syntax.all.*

// `combine` (aliased to |+|) and `empty` come from the Monoid instance
println(1 |+| 2)
println("type" |+| "level")
println(Monoid[List[Int]].empty)

// folding a collection with its monoid
println(List(1, 2, 3, 4).combineAll)
println(List("a", "b", "c").combineAll)

// tuples and maps are monoids when their contents are
println(List(("cats", 1), ("cats", 2)).combineAll)
println(List(Map("a" -> 1), Map("a" -> 2, "b" -> 3)).combineAll)
30 changes: 30 additions & 0 deletions src/tutorials/directory.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Interactive tutorials (prototype).
#
# How a tutorial works:
# * every lesson is a markdown file whose prose is rendered into the left
# pane by /tutorials/tutorial.template.html
# * next to it lives a `.sc` file with the sample code, which tutorials.js
# loads into an embedded Scastie editor in the right pane (the scala-cli
# script extension, since the samples are worksheet-style: top-level
# statements plus `//> using` directives, runnable with `scala run`)
# * each track is a subdirectory; its directory.conf points the lessons at
# the tutorial template and fixes their order
#
# Restricting the target formats to HTML keeps these pages out of the search
# index and the RSS feed, so the prototype stays unlisted (nothing on the site
# links to /tutorials either) until we decide it is ready to announce.
laika.targetFormats = [html]

# Defaults for the embedded Scastie editor. Tracks and individual lessons can
# override any of these. Dependencies are declared with `//> using` directives
# in the lesson's code file, which is why the target is Scala CLI.
#
# The target names Scastie accepts are "scala-cli", "scala3", "jvm" (Scala 2),
# "typelevel", "js" and "native"; anything else is quietly ignored and you end
# up with a plain sbt Scala 3 editor where `//> using` does nothing. Leaving
# scala-version empty takes Scastie's current default.
scastie {
target-type = "scala-cli"
scala-version = ""
worksheet = true
}
61 changes: 61 additions & 0 deletions src/tutorials/tutorial.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@:embed(/templates/main.template.html)
<!--
Two-pane tutorial lesson: prose on the left, a live Scastie editor on the
right. The page shares the site-wide main.template.html unchanged, which is
also why the stylesheet is linked here in the body rather than in <head>.

The sample code is not inlined: `scastie.code` names a .sc file sitting next
to the lesson's markdown, and tutorials.js fetches it. That keeps the code
free of HTML escaping and lets it be edited (and run) as real Scala.
-->
<link rel="stylesheet" href="@:target(/tutorials/tutorials.css)">

<div class="tutorial-layout">

<section class="tutorial-prose">
<div class="tutorial-prose-body bulma-content">
<h1 class="bulma-title">${cursor.currentDocument.title}</h1>
${cursor.currentDocument.content}
</div>

<nav class="tutorial-nav" aria-label="lesson navigation">
<div class="tutorial-nav-slot">
@:for(cursor.previousDocument)
<a class="bulma-button bulma-is-light" href="${_.path}" rel="prev">
<span aria-hidden="true">&larr;&nbsp;</span>${_.title}
</a>
@:@
</div>
<div class="tutorial-nav-slot tutorial-nav-centered">
<a class="bulma-button bulma-is-light" href="@:target(/tutorials/README.md)" rel="up">All tutorials</a>
</div>
<div class="tutorial-nav-slot tutorial-nav-end">
@:for(cursor.nextDocument)
<a class="bulma-button bulma-is-primary" href="${_.path}" rel="next">
${_.title}<span aria-hidden="true">&nbsp;&rarr;</span>
</a>
@:@
</div>
</nav>
</section>

<section class="tutorial-editor">
<div class="tutorial-editor-bar">
<span class="tutorial-editor-label">Try it yourself</span>
<button type="button" id="open-in-scastie" class="bulma-button bulma-is-small bulma-is-light">
Open in Scastie
</button>
</div>
<div class="tutorial-editor-pane">
<pre id="tutorial-code" class="tutorial-code" data-code-src="${?scastie.code}"
data-target-type="${?scastie.target-type}" data-scala-version="${?scastie.scala-version}"
data-worksheet="${?scastie.worksheet}">Loading the editor&hellip;</pre>
</div>
<div id="tutorial-toast" class="tutorial-toast" role="status" hidden></div>
</section>

</div>

<script defer src="https://scastie.scala-lang.org/embedded.js"></script>
<script defer src="@:target(/tutorials/tutorials.js)"></script>
@:@
Loading
Loading