Skip to content

Commit a6e7132

Browse files
committed
Add nested HTML exercise
This is an example of indexed codata with a bit inspiration thrown in Fix some typos
1 parent 6378cbd commit a6e7132

File tree

1 file changed

+110
-7
lines changed

1 file changed

+110
-7
lines changed

src/pages/indexed-types/codata.md

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ sealed trait TitleState
193193
trait WithoutTitle extends TitleState
194194
trait WithTitle extends TitleState
195195

196+
// Not a case class so external users cannot copy it
197+
// and break invariants
196198
final class Html[S <: StructureState, T <: TitleState](
197199
head: Vector[String],
198200
body: Vector[String]
@@ -227,18 +229,19 @@ final class Html[S <: StructureState, T <: TitleState](
227229
val h = head.mkString(" <head>\n ", "\n ", "\n </head>")
228230
val b = body.mkString(" <body>\n ", "\n ", "\n </body>")
229231

230-
s"\n<html>\n$h\n$b\n</head>"
232+
s"\n<html>\n$h\n$b\n</html>"
231233
}
232234
}
233235
object Html {
234236
val empty: Html[Empty, WithoutTitle] = Html(Vector.empty, Vector.empty)
235237
}
236238
```
237239

238-
The important detail is that we factor the state into two components.
239-
One represents where in the overall structure we are (inside the `head`, inside the `body`, or inside neither).
240-
The other represents whether we have a `title` element or not.
241-
We could certainly represent this with one state type variable, but I find the factored representation easier to work with.
240+
The key point is that we factor the state into two components.
241+
`StructureState` represents where in the overall structure we are (inside the `head`, inside the `body`, or inside neither).
242+
`TitleState` represents the state when defining the elements inside the `head`, specifically whether we have a `title` element or not.
243+
We could certainly represent this with one state type variable, but I find the factored representation both easier to work with and easier for other developers to understand.
244+
We can implement more complex protcols, such as those that can be represented by context-free or even context-sensitive grammars, using the same technique.
242245

243246
Here's an example in use.
244247

@@ -261,9 +264,109 @@ Html.empty.head
261264
.h1("This Shouldn't Work")
262265
```
263266

264-
(Note that the error messages are not great. We'll address this in Chapter [@sec:usability]).
267+
These error messages are not great. We'll address this in Chapter [@sec:usability].
265268

266-
We can implement more complex protcols, such as those that can be represented by context-free or even context-sensitive grammars, using the same technique.
269+
270+
#### Exercise: HTML API Design {-}
271+
272+
I don't particularly like the HTML API we developed above,
273+
as the flat method call structure doesn't match the nesting in the HTML structure we're creating.
274+
I would prefer to write the following.
275+
276+
```scala
277+
Html.empty
278+
.head(_.title("Our Amazing Webpage"))
279+
.body(_.h1("Where Amazing Happens").p("Right here"))
280+
.toString
281+
```
282+
283+
We still require the head is specified before the body,
284+
but now the nesting of the method calls matches the nesting of the structure.
285+
Notice we're still using a Church-encoded representation.
286+
287+
Can you think of how to implement this?
288+
You'll need to use indexed codata, and perhaps a bit of inspiration.
289+
This is a very open ended question, so don't worry if you struggle with it!
290+
291+
<div class="solution">
292+
Here's how I implemented it.
293+
The structure is very similar to the original implementation,
294+
but where we factored the state into type parameters
295+
I also factored the implementation into types.
296+
Notice how we use `Head` and `Body` to accumulate the set of tags that make up the head and body respectively.
297+
We still need to use indexed codata in some place, but we can avoid it in others.
298+
For example, the `head` method simply requires a function of type `Head[WithoutTitle] => Head[WithTitle]`.
299+
300+
```scala mdoc:reset:silent
301+
sealed trait StructureState
302+
trait NeedsHead extends StructureState
303+
trait NeedsBody extends StructureState
304+
trait Complete extends StructureState
305+
306+
sealed trait TitleState
307+
trait WithoutTitle extends TitleState
308+
trait WithTitle extends TitleState
309+
310+
final class Head[S <: TitleState](contents: Vector[String]) {
311+
def title(text: String)(using S =:= WithoutTitle): Head[WithTitle] =
312+
Head(contents :+ s"<title>$text</title>")
313+
314+
def link(rel: String, href: String): Head[S] =
315+
Head(contents :+ s"<link rel=\"$rel\" href=\"$href\"/>")
316+
317+
override def toString(): String =
318+
contents.mkString(" <head>\n ", "\n ", "\n </head>")
319+
}
320+
object Head {
321+
val empty: Head[WithoutTitle] = Head(Vector.empty)
322+
}
323+
324+
final class Body(contents: Vector[String]) {
325+
def h1(text: String): Body =
326+
Body(contents :+ s"<h1>$text</h1>")
327+
328+
def p(text: String): Body =
329+
Body(contents :+ s"<p>$text</p>")
330+
331+
override def toString(): String =
332+
contents.mkString(" <body>\n ", "\n ", "\n </body>")
333+
}
334+
object Body {
335+
val empty: Body = Body(Vector.empty)
336+
}
337+
338+
final class Html[S <: StructureState](
339+
head: Head[?],
340+
body: Body
341+
) {
342+
def head(f: Head[WithoutTitle] => Head[WithTitle])(using
343+
S =:= NeedsHead
344+
): Html[NeedsBody] =
345+
Html(f(Head.empty), body)
346+
347+
def body(f: Body => Body)(using S =:= NeedsBody): Html[Complete] =
348+
Html(head, f(Body.empty))
349+
350+
override def toString(): String = {
351+
s"\n<html>\n${head.toString()}\n${body.toString()}\n</html>"
352+
353+
}
354+
}
355+
object Html {
356+
val empty: Html[NeedsHead] = Html(Head.empty, Body.empty)
357+
}
358+
```
359+
360+
As always, we should show that is works.
361+
Here's the output from the motivating example.
362+
363+
```scala mdoc
364+
Html.empty
365+
.head(_.title("Our Amazing Webpage"))
366+
.body(_.h1("Where Amazing Happens").p("Right here"))
367+
.toString()
368+
```
369+
</div>
267370

268371
[html]: https://html.spec.whatwg.org/multipage/
269372

0 commit comments

Comments
 (0)