Haskell is a statically typed programming language. The type system is a central part of the language and applies to all program elements, even at the most granular level. We'll take a look at how to implement the simple-moving-average function using this system:
module Main where import Data.List.Split(chop) type Prices = [Float] type AveragePrices = [Float] loadPrices :: FilePath -> IO Prices loadPrices path = do priceContents <- readFile path let priceLines = (lines priceContents) return (map (x -> read x :: Float) priceLines) divvy :: Int -> Int -> [a] -> [[a]] divvy n m lst = filter (ws -> (n == length ws)) choppedl where choppedl = chop (xs -> (take n xs , drop m xs)) lst average :: [Float] -> Float average xs = sum xs / (fromIntegral $ length xs) simpleMovingAverage :: Prices -> AveragePrices simpleMovingAverage priceList = map average divvyedPrices where divvyedPrices = divvy 20 1 priceList main :: IO () main = do priceList <- loadPrices "prices.txt" let priceAverages = simpleMovingAverage priceList print priceAverages
Let's start at the bottom of the file, the main
function. This is the entry point of a program. The main :: IO ()
expression names the function. The part to the right-hand side of ::
declares that the function will return an effective I/O operation, which returns a ()
Unit type. A Unit type is akin to a void
type in Java, which simply means that the function doesn't return anything. By introducing IO
and its effects, we have to understand that Haskell has a strict policy of containing any operation that doesn't return the value of its evaluation. These are known as impure operations, or side effects.
Pure functional languages are those that do not have any side effects. The computation model is composed entirely of program elements whose operations can be guaranteed at compile time. The goal is to make more provable systems. The result of this is that code tends to be more declarative, as we can see in the preceding Haskell example.
Most Haskell functions return the result of one nested evaluation block. The main
function evaluation occurs in a do block
that lets us evaluate several expressions one after the other. loadPrices
pulls in a large text of price data. Loading data from a filesystem is an operation with side-effects because data is being loaded from outside the system's runtime environment:
5.776354340433784 4.819385095126501 4.024983019350133 3.6118300426230148 3.69365835786349 4.247942377070631 5.1220995447382265 6.0754930574679324
We then bind that result to priceList
, which is then used in our simpleMovingAverage
function, the result of which is printed to the console (the IO ()
operation). loadPrices
then divides this data into lines and maps a function over this list to convert each text line into a numeric value (Float
). Something to note about loadPrices
is that it explicitly declares the type of data you can input, and the type of data that will be returned. loadPrices :: FilePath -> IO Prices
declare the function, and FilePath
and Prices
are types that we've declared in the previous section. Both types are lists of float
values. The IO
qualifier again declares an effectful operation that will be returned, only this time on Prices
(a list of Float
values).
Simply by looking at the main
and loadPrices
functions, we already have an idea of Haskell's general computation model. The type system is pervasive and is used all the way down to the most granular program elements. This is a tool meant to maintain pure operations that are free of side effects. Any required side effects are contained to effectful types and explicitly declared when using these types. The result is more compile-time guarantees of program correctness, and peripherally, a more declarative code style. Clojure is not a pure functional language because it allows side effects where they are pragmatically needed. It is also more loosely typed as program element types do not have to be declared and arranged at compile time. Clojure veers toward fewer types that are consistent and can represent a wider array of data. This also means more functions and more general functions can operate on this data. While Clojure is more declarative than Java, it emphasizes List processing (how we get the name Lisp). You can read more about Lisp at https://en.wikipedia.org/wiki/Lisp_(programming_language). Lisp's macro system, or the ability to rewrite code on the fly, came out of its data-centric and homoiconic property. It's also what enables Lisps and Clojure, in this case, to have several third-party options for static analysis. core.contracts
(you can read more at https://github.com/clojure/core.contracts) and core.typed
(you can read more at https://github.com/clojure/core.typed), for example, are just two third-party tools that offer static code analysis using the
design by contract and
gradual typing approaches, respectively.
Haskell's more declarative code style is evident in the simpleMovingAverage
function. The simpleMovingAverage :: Prices -> AveragePrices
declaration tells us that it takes the Prices
type (a list of Float
values) and returns the AveragePrices
type (also a list of Float
values). map
acts the same as it does in Clojure, mapping the average
function over the divvyedPrices
list. The divvyedPrices
list is created from the original priceList
using the divvy
function we've created. Our divvy
function behaves in the same way as the Clojure partition
function, returning a list of n
items, where the start of each partition is separated by m
elements. The average
function simply takes a list of Float
values and divides their sum by their size.
The remaining import
function simply pulls in the chop
function from the Data.List.Split
module, and the module
declaration is Haskell's way of grouping program elements. These include functions, data, type definitions, and so on. It's Haskell's namespacing facility and has the same purpose as Clojure's ns
macro. I'm using a Haskell build tool called
cabal
(you can read more at https://www.haskell.org/cabal). If we compile our Haskell file, we'll get an executable that runs, using the the prices.txt
file, as input:
\ cabal build Building lagging-haskell-0.1.0.0... Preprocessing executable 'lagging-haskell' for lagging-haskell-0.1.0.0... [1 of 1] Compiling Main ( Main.hs, dist/build/lagging-haskell/lagging-haskell-tmp/Main.o ) Linking dist/build/lagging-haskell/lagging-haskell ... \ cabal run Preprocessing executable 'lagging-haskell' for lagging-haskell-0.1.0.0... Running lagging-haskell... [7.260286,7.70551,8.211639,8.776141,9.384563,10.015532,10.6473875,11.2645855,11.862074,12.446443,13.012064,13.83844,14.66226,15.385584,16.131344,17.447102,18.75085,19.480864,20.09607,20.608353,21.086105,21.557089,22.058533,22.446215,22.703249,22.940306,23.395824,23.822289,24.226925,24.604544,24.973045,25.071888,25.18419,25.378178,25.50724,25.209455,24.908524,24.662724,24.543427,24.55034,24.591665,24.775486,24.829182,24.952164,25.303926,25.511312,25.535767,25.611546,25.547924,25.61414,25.650028,25.625921,25.760883,25.800354,25.855785,25.97283,26.023987,26.002125,26.020596,26.103607,26.15481,25.97431,25.890036,25.923187,25.84811,25.910746,25.907635,25.80542,25.764154,25.519506,25.314304,25.315907,25.229626,25.154488,24.970709,24.779993,24.790949,24.897942,24.835285,24.599857,24.464247]
3.133.156.251