Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save giuliohome/2a93efd36de6176df10bce0db2a292a7 to your computer and use it in GitHub Desktop.
Save giuliohome/2a93efd36de6176df10bce0db2a292a7 to your computer and use it in GitHub Desktop.
Sample gist showing how to run a HTTP server with Typelevel Scala libraries, and a postgres docker container
//> 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)))
@giuliohome
Copy link
Author

giuliohome commented Sep 30, 2023

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!

@hmemcpy
Copy link

hmemcpy commented Sep 30, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment