Created
July 15, 2012 01:45
-
-
Save quelgar/3114349 to your computer and use it in GitHub Desktop.
Simple Scala example of a pure functional program that does I/O
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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