-
-
Save giuliohome/2a93efd36de6176df10bce0db2a292a7 to your computer and use it in GitHub Desktop.
//> using dep "org.http4s::http4s-scalatags::0.25.2" | |
//> using dep "org.http4s::http4s-dsl::0.23.23" | |
//> using dep "org.http4s::http4s-ember-server::0.23.23" | |
//> using dep "org.tpolecat::skunk-core::0.6.0" | |
//> using dep "com.dimafeng::testcontainers-scala-postgresql::0.41.0" | |
//> using dep "com.outr::scribe-slf4j::3.12.2" | |
import skunk.*, codec.all.*, syntax.all.* | |
import cats.effect.* | |
import scalatags.Text.all.* | |
import org.http4s.{HttpRoutes} | |
import org.http4s.scalatags.* | |
import org.http4s.dsl.io.* | |
import org.http4s.ember.server.EmberServerBuilder | |
import com.comcast.ip4s.* | |
import com.dimafeng.testcontainers.PostgreSQLContainer | |
import org.testcontainers.utility.DockerImageName | |
import natchez.Trace | |
import cats.syntax.all.* | |
enum EmailAddress: | |
case Private | |
case Public(email: String) | |
def toOption = | |
this match | |
case Private => None | |
case Public(email) => Some(email) | |
object EmailAddress: | |
def from(s: Option[String]) = | |
s match | |
case None => EmailAddress.Private | |
case Some(value) => EmailAddress.Public(value) | |
case class User(id: Int, username: String, email: EmailAddress) | |
object User: | |
val codec = | |
(int4 *: varchar *: varchar.opt.imap(EmailAddress.from)(_.toOption)) | |
.to[User] | |
def main(a: scalatags.Text.Modifier*) = | |
html(body(a*)) | |
def queryUserById(id: Int)(using sess: Session[IO]) = | |
val query = sql"select * from users where id = $int4" | |
sess.option(query.query(User.codec))(id) | |
val userNotFoundPage = | |
p("This user does not exist :(") | |
def userView(user: User) = | |
main( | |
p(s"User ID: ${user.id}"), | |
p(s"Username: ${user.username}"), | |
user.email match | |
case EmailAddress.Private => p("This user has a private email address") | |
case EmailAddress.Public(email) => p(s"Email address: $email") | |
) | |
def renderUserOrNotFound(potentialUser: Option[User]) = | |
potentialUser.map(userView).getOrElse(userNotFoundPage) | |
def renderUserHandler(userId: Int)(using Session[IO]) = | |
queryUserById(userId).map(renderUserOrNotFound) | |
def handler(using Session[IO]) = | |
HttpRoutes.of[IO] { case GET -> Root / "users" / IntVar(i) => | |
renderUserHandler(i).flatMap(Ok(_)) | |
} | |
object App extends IOApp.Simple: | |
val run = | |
skunkConnection(using Trace.Implicits.noop) | |
.flatMap { case given Session[IO] => | |
EmberServerBuilder | |
.default[IO] | |
.withHttpApp(handler.orNotFound.onError { case err => | |
cats.data.Kleisli(req => IO(scribe.error(s"[$req] Error: ", err))) | |
}) | |
.withPort(port"9955") | |
.build | |
} | |
.use(server => | |
IO.println( | |
s"Server running at ${server.baseUri}, press Enter to terminate" | |
) *> IO.readLine | |
) | |
.void | |
private def postgresContainer = | |
Resource.make( | |
IO( | |
PostgreSQLContainer( | |
dockerImageNameOverride = DockerImageName("postgres:14"), | |
mountPostgresDataToTmpfs = true | |
) | |
).flatTap(cont => IO(cont.start())) | |
)(cont => IO(cont.stop())) | |
end postgresContainer | |
private def skunkConnection(using natchez.Trace[IO]) = | |
postgresContainer | |
.evalMap(cont => parseJDBC(cont.jdbcUrl).map(cont -> _)) | |
.flatMap { case (cont, jdbcUrl) => | |
Session.single[IO]( | |
host = jdbcUrl.getHost(), | |
port = jdbcUrl.getPort(), | |
user = cont.username, | |
password = Some(cont.password), | |
database = cont.databaseName | |
) | |
} | |
.evalTap { sess => | |
val commands = Seq( | |
sql"DROP TABLE IF EXISTS users".command, | |
sql"CREATE TABLE users (id serial, username varchar not null, email varchar)".command, | |
sql"INSERT INTO users(username, email) values ('anton', 'bla@bla.com')".command, | |
sql"INSERT INTO users(username, email) values ('dark_anton', NULL)".command | |
) | |
commands.traverse(sess.execute) | |
} | |
private def parseJDBC(url: String) = IO(java.net.URI.create(url.substring(5))) |
I am mentioned, so I'll answer: practice makes perfect. Sure, there's probably no need for all that to spin a webserver that renders some HTML. I'm sure there are magic frameworks out there that can do this with a one-liner. However, the practice of functional programming is one that deliberately restricts the infinite amount of possibility into a strict subset of programming. What once was dubbed the Scalazzi Subset is a set of principles, which at the heart wants to say one thing: disallow things you cannot control.
By programming in this style, every single expression is a value. This value, on its own, does nothing except describing the intended outcome at some later time when the program is executed by the runtime. Such descriptions, being values, are highly composable, each individually self-defined (defining precisely its own inputs and outputs). Values are pure and immutable - they do not have side effects or undefined behaviors. They do not cause surprises at runtime. I would argue that the runtime makes no difference - the program is perceived "correct" at every level until "the end of the world" (aka, the main
function). What happens beyond the main is no longer the programmer's concern.
To summarize this mini-blogpost: programming in this style frees the user of a huge amount of issues that commonly plague other programming languages and paradigms. Not even things like nulls or exceptions - those are the least interesting concerns here. But the ability to compose/combine and reason about each piece individually, without ever running the code once, and knowing exactly the outcome that will be produced by the whole - is extremely liberating and very hard to dismiss once you reap the benefits, even for tiny programs such as this one.
My tweet was obviously a joke, but the quoted tweets follow a similar structure, both in Kotlin, Scala, and the F# examples: a fully working program composed from small, self-contained pieces. In Scala, it takes one step further by only utilizing purely functional constructs that do not have any side effects, but the idea is the same. Composition and reasoning are the ultimate goals of functional programming.
Hope this clarifies it.
Thank you @keynmol for sharing this excellent example, especially for the twitter thread, extremely clear and informative!
I have a "philosophical" question, I take also another repository as a reference of a Scala full-stack app: https://github.com/keynmol/twotm8
Why struggle with using Typelevel (and behind that, some complex math concepts like Kleisli, etc.) if, in the end, you have to rely on trivial SQL for the core business logic of the app? Here, it's a simple CRUD, but further up in the mentioned app, you're still resorting to traditional joins and ugly subqueries, (e.g. see https://github.com/keynmol/twotm8/blob/main/app/src/main/scala/db.scala#L223 ), like what a typical old-fashioned object-oriented programmer would have done. So, why go through the struggle of using Typelevel, just for the sake of applying some 'cool' representation of category theory? Similar comment but in reply to @hmemcpy here https://x.com/jcubic/status/1707520567237046696 "This is not Haskell only Type Theory Math". By the way, the awesome Google Bard is perfectly capable of analysing the image in his tweet! https://g.co/bard/share/31548d370652
I've noticed that @keynmol you've written an awesome blog post about bringing Scala to Cloudflare workers! Once again, it seems you must have a strong dislike for JavaScript to go to the lengths of implementing a complex workaround to obtain an isomorphic Scala source. However, it's interesting to observe that (here above) you're perfectly comfortable with embracing what some might consider 'ugly' SQL. It's worth noting that in the .NET world, of which the F# code (translated to Scala by you) is a part, we also have tools like Entity Framework and similar ORMs (my old favorite Linq2db or the more F# idiomatic SQLProvider) to abstract away the complexities of SQL, allowing developers to write C# or F# for the persistence layer, much like how Scala.js transpiles to JavaScript on the frontend. For Kotlin see @nomisRev version with SqlDelight!