Comparing Clojure with FP and strong typing

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]
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.156.251