I'm releasing the typed-spreadsheet
library, which lets you build spreadsheets integrated with Haskell. I use the term "spreadsheet" a bit loosely, so I'll clarify what I mean by comparing and contrasting this library with traditional spreadsheets.
The best way to explain how this works is to begin with a small example:
{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative
import Typed.Spreadsheet
main :: IO ()
main = textUI "Example program" logic
where
-- Hate weird operators? Read on!
logic = combine <$> checkBox "a" -- Input #1
<*> spinButton "b" 1 -- Input #2
<*> spinButton "c" 0.1 -- Input #3
<*> entry "d" -- Input #4
combine a b c d = display (a, b + c, d) -- The output is a
-- function of all
-- four inputs
The above program builds a graphical user interface with four user inputs in the left panel and one output in the right panel:
The output is a text representation of a 3-tuple whose:
- 1st element is the checkbox state (
False
for unchecked,True
for checked) - 2nd element is the sum of the two numeric fields (labeled "b" and "c")
- 3rd element is the text entry field
The right panel immediately updates in response to any user input from the left panel. For example, every time we toggle the checkbox or enter numbers/text the right panel changes:
So in one sense this resembles a spreadsheet in that the output "cell" on the right (the text panel) updates immediately in response to the input "cell"s on the left (the checkbox, and numeric/text entry fields).
However, this also departs significantly from the traditional spreadsheet model: input controls reflect the type of input in order to make invalid inputs unrepresentable. For example, a Bool
input corresponds to a checkbox.
Distribution
The generated executable is an ordinary binary so you can distribute the program to other users without needing to supply the Haskell compiler or toolchain. You can even fully statically link the executable for extra portability.
For example, say that I want to create a mortage calculator for somebody else to use. I can just write the following program:
{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative
import Data.Monoid
import Data.Text (Text)
import Typed.Spreadsheet
payment :: Double -> Double -> Double -> Text
payment mortgageAmount numberOfYears yearlyInterestRate
= "Monthly payment: $"
<> display (mortgageAmount * (i * (1 + i) ^ n) / ((1 + i) ^ n - 1))
where
n = truncate (numberOfYears * 12)
i = yearlyInterestRate / 12 / 100
logic :: Updatable Text
logic = payment <$> spinButton "Mortgage Amount" 1000
<*> spinButton "Number of years" 1
<*> spinButton "Yearly interest rate (%)" 0.01
main :: IO ()
main = textUI "Mortgage payment" logic
... and compile that into an executable which I can give them. When they run the program they will get the following simple user interface:
Or maybe I want to write a simple "mad libs" program for my son to play:
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid
import Typed.Spreadsheet
noun = entry "Noun"
verb = entry "Verb"
adjective = entry "Adjective"
example =
"I want to " <> verb <> " every " <> noun <> " because they are so " <> adjective
main :: IO ()
main = textUI "Mad libs" example
This generates the following interface:
All the above examples have one thing in common: they only generate a single Text
output. The typed-spreadsheet
library does not permit multiple outputs or outputs other than Text
. If we want to display multiple outputs then we need to somehow format and render all of them within a single Text
value.
In the future the library may provide support for diagrams
output instead of Text
but for now I only provide Text
outputs for simplicity.
Applicatives
The central type of this library is the Updatable
type, which implements the Applicative
interface. This interface lets us combine smaller Updatable
values into larger Updatable
values. For example, a checkBox
takes a single Text
argument (the label) and returns an Updatable
Bool
:
checkBox :: Text -> Updatable Bool
Using Applicative
operators, (<$>)
and (<*>)
, we can lift any function over an arbitrary number of Updatable
values. For example, here is how I would lift the binary (&&)
operator over two check boxes:
z :: Updatable Bool
z = (&&) <$> checkBox "x" <*> checkBox "y"
... or combine their output into a tuple:
both :: Updatable (Bool, Bool)
both = (,) <$> checkBox "x" <*> checkBox "y"
However, to the untrained eye these will look like operator soup. Heck, even to the trained eye they aren't that pretty (in my opinion).
Fortunately, ghc-8.0
will come with a new ApplicativeDo
which will greatly simplify programs that use the Applicative
interface. For example, the above two examples would become much more readable:
z :: Updatable Bool
z = do
x <- checkBox "x"
y <- checkBox "y"
return (x && y)
both :: Updatable (Bool, Bool)
both = do
x <- checkBox "x"
y <- checkBox "y"
return (x, y)
Similarly, the very first example simplifies to:
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE OverloadedStrings #-}
import Typed.Spreadsheet
main :: IO ()
main = textUI "Example program" (do
a <- checkBox "a"
b <- spinButton "b" 1
c <- spinButton "c" 0.1
d <- entry "d"
return (display (a, b + c, d)) )
That's much easier on the eyes. ApplicativeDo
helps the code look much less like operator soup and presents a comfortable syntax for people used to imperative programming.
Conclusion
Spreadsheet integration with Haskell comes with advantages and disadvantages.
The obvious advantage is that you get the full power of the Haskell ecosystem. You can transform input to output using arbitrary Haskell code. You also get the benefit of the strong type system, so if you need extra assurance for critical calculations you can build that into your program.
The big disadvantage is that you have to relaunch the application in order to change the code. The library does not support live code reloading. This is technically feasible but requires substantially more work and would make the application much less portable.
If you follow my previous work this is very similar to a previous post of mine on spreadsheet-like programming in Haskell. This library simplifies many of the types and ideas from that previous post and packages them in a polished library.
If you would like to contribute to this library there are two main ways that you can help:
- Adding new types of input controls
- Adding new backends for output (like
diagrams
)
If you would like to use this code you can find the library on Github or Hackage.
This comment has been removed by the author.
ReplyDeleteCool. I can imagine nice combination with Broadway to serve the results https://developer.gnome.org/gtk3/3.5/gtk-broadway.html
ReplyDeleteThe idea of diagrams output: is it just an idea or are you working on it?
I went ahead and added a diagrams backend just because you asked :)
Deletehttps://github.com/Gabriel439/Haskell-Typed-Spreadsheet-Library/commit/14b1be04f7b04334c97f23f66c4bc6106d7cb8e7
There's an example under `exec/Graphics.hs` showing how to use it. I'll add more documentation in the near future
I also got Broadway to work, too. Pretty neat!
Wunderbar!
Delete(please increase version number in such a changes so it's possible to specify right dependencies. I have made a twoliner pullrequest for it. Thanks.)
Do you have any pointers on a good way to try this out with Stack? Is it best to create a stack managed project? or are there any other light ways to try it? Thanks!
ReplyDeleteTo build it and hack yourself or just to use as an dependency in your project?
DeleteI replied by e-mail because I couldn't comment on my own blog at the time, but I wanted to mention my response again here for the benefit of others now that I can comment.
Delete`typed-spreadsheet` is `stack`-enabled and there are setup instructions for Debian and OS X in the README: https://github.com/Gabriel439/Haskell-Typed-Spreadsheet-Library/#quick-start