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)))
@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