Create a gist now

Instantly share code, notes, and snippets.

Simple Scala example of a pure functional program that does I/O
/*
* Referentially transparent program to print the size of files.
*
* Techniques inspired by:
* "Dead-Simple Dependency Injection"
* Rúnar Óli Bjarnason
* Northeast Scala Symposium, 2012
*
* To run: "scala filesizerer.scala"
* When prompted, enter a file name.
* If the file exists, its size will be printed,
* otherwise prints "Size: Unknown"
*/
/**
* An IO action to be performed.
* Every action has a "link" to the next step,
* forming a chain of actions.
* IOAction is a functor.
*/
sealed abstract class IOAction[B] {
protected type A
protected val link: A => B
protected def dup[C](f: A => C): IOAction[C]
final def map[C](f: B => C): IOAction[C] = dup(f compose link)
}
final case class ReadConsole[B](protected val link: String => B) extends IOAction[B] {
protected type A = String
protected def dup[C](f: String => C) = ReadConsole(f)
}
final case class WriteConsole[B](protected val link: Unit => B, msg: String) extends IOAction[B] {
protected type A = Unit
protected def dup[C](f: Unit => C) = WriteConsole(f, msg)
}
final case class FileSize[B](protected val link: Option[Long] => B, file: String) extends IOAction[B] {
protected type A = Option[Long]
protected def dup[C](f: Option[Long] => C) = FileSize(f, file)
}
/**
* A monad for limited I/O. The dependency on IOAction can be abstracted over,
* which gives you a "free" monad for any functor,
* but I left that out to keep the demo simple.
*/
sealed abstract class SimpleIO[A] {
def flatMap[B](f: A => SimpleIO[B]): SimpleIO[B]
final def map[B](f: A => B): SimpleIO[B] = flatMap((IODone.apply[B] _) compose f)
/**
* A convenience version of flatMap that ignores the result of the
* preceeding action (which is commonly Unit anyway).
* Like a functional version of Java's semicolon.
* `>>` in Haskell.
*/
final def andThen[B](io: SimpleIO[B]) = flatMap(Function.const(io))
}
/**
* The end of a chain of actions holding the resulting value of the chain.
*/
final case class IODone[A](a: A) extends SimpleIO[A] {
def flatMap[B](f: A => SimpleIO[B]) = f(a)
}
/**
* Represents more actions to be performed.
*/
final case class IOMore[A](next: IOAction[SimpleIO[A]]) extends SimpleIO[A] {
def flatMap[B](f: A => SimpleIO[B]) = IOMore(next.map(_ flatMap f))
}
val readConsole = IOMore[String](ReadConsole(IODone.apply _))
def writeConsole(msg: String) = IOMore[Unit](WriteConsole(IODone.apply _, msg))
def fileSize(file: String) = IOMore[Option[Long]](FileSize(IODone.apply _, file))
val terminate = IODone(())
/**
* The part of our program that repeats, looping using recursion.
*/
val loop: SimpleIO[Unit] = for {
_ <- writeConsole("Enter a file name or q to quit:")
s <- readConsole
_ <- if ("q" equalsIgnoreCase s) {
terminate
}
else for {
size <- fileSize(s)
_ <- writeConsole("Size: " +
(size map ("%,12d".format(_)) getOrElse ("%12s".format("Unknown"))))
_ <- loop
} yield ()
} yield ()
/**
* Our referentially transparent program.
*/
val program: SimpleIO[Unit] =
writeConsole("Welcome to file sizerer") andThen loop
/*
All the above code is referentially transparent.
Only the code below has side-effects.
*/
/**
* A "runner" to execute any `SimpleIO` program.
* We could write other implementations, such as a mock
* runner for unit tests, or a GUI, or using HTTP instead
* of the file system. You get the idea.
*/
@annotation.tailrec
def run[A](p: SimpleIO[A]): A = {
p match {
case IODone(a) => a
case IOMore(ReadConsole(link)) => run(link(readLine()))
case IOMore(WriteConsole(link, msg)) => {
println(msg)
run(link(()))
}
case IOMore(FileSize(link, name)) => {
val f = new java.io.File(name)
val length = f.length
run(link(if (length == 0) None else Some(length)))
}
}
}
run(program)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment