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