| I recently saw the title, Learn Physics with Functional Programming, and thought to myself... I know physics and functional programming but I want to learn Haskell. Having gone through it, I highly recommend the book; especially to anyone knowledgeable about any 2 and interested in the third. I love Haskell. I love writing it, and reading it. Haskell is a beautiful programming language. One where I felt immediately at home in and comforted by. Currying is one of the foundational concepts of the language, but I do think it is one aspect of the language the community treats excessively sacredly. In Haskell, function composition has right associativity. What this means, in the event of a function that takes in 3 Integers as arguments and returns a Double would have a type definition of: FN_NAME :: Int -> Int -> Int -> Double
The right associativity then also means this is equivalent to these type definition: fn0 :: Int -> Int -> Int -> Double
fn0 x y z = (fromIntegral (x + y)) / (fromIntegral z)
fn1 :: Int -> (Int -> Int -> Double)
fn1 x y z = (fromIntegral (x + y)) / (fromIntegral z)
fn2 :: Int -> (Int -> (Int -> Double))
fn2 x y z = (fromIntegral (x + y)) / (fromIntegral z)
ghci> fn0 1 2 3 -- 1.0
ghci> fn1 1 2 3 -- 1.0
ghci> fn2 1 2 3 --1.0
This is currying in action. A function that takes three arguments is the same as a function that takes one argument and passes it to function that takes two arguments.As I understand it, the compiler sees ALL functions as a composition of functions of one input. This is pretty cool, and a powerful foundation to build on. Where I take a bit of issue is when the Haskell community allows the rigor of the type definition to carry some of the weight of the expressiveness of the function definition. Each function definition ('=') pairs with a type definition ('::'), as the ones above, ie. an example from the promoted book: type R = Double
type VecDerivative = (R -> Vec) -> R -> Vec
vecDerivative :: R -> VecDerivative
vecDerivative dt v t = (((v (t + dt/2)) ^-^ (v (t - dt/2))) ^/ dt)
velFromPos :: R -- dt
-> (R -> Vec) -- position function
-> (R -> Vec) -- velocity function
velFromPos = vecDerivative
The second function definition: "velFromPos = vecDerivative" ; is equal to saying, "velFromPos dt v t = vecDerivative dt v t". Haskell seems to allows this syntactical ~implicit argument passing because of it's reverence for currying. -- random example of 'vector-valued function'
v1 :: R -> Vec
v1 t = ((2 *^ (t**2 *^ iHat)) ^+^ (3 *^ (t**3 *^ jHat)) ^+^ (t**4 *^ kHat))
ghci> vecDerivative 0.01 v1 1 -- vec 3.999999999999937 9.000074999999885 4.000099999999962
ghci> velFromPos 0.01 v1 1 -- vec 3.999999999999937 9.000074999999885 4.000099999999962
This has some interesting effects, such as displayed above where the use of currying makes it explicitly clear that calculating the velocity is the exact same thing as calculating the derivative of position.But I always felt this ~implicit syntax is needlessly aggressive to newcomers. I also pathologically write my Haskell code like a Lisp because the Haskell community's celebration of the complex precedence structure of its order of operations causes practitioners to appear, to my eyes, over confident in their ability to sight read their own work's order of operations. ;P ghci> sqrt 2 + 1 * 3 + 3 * 2 + 1 / 7 -- 10.557070705230238
ghci> ((sqrt 2) + ((1 * 3) + ((3*2) + (1 / 7)))) -- 10.557070705230238
I will forever think the second is easier to read, but if you look at many Haskell code bases you will see stuff like the first all over the place, and I have been unable to find a reason why the implicit function arguments and lack of parenthesis syntax is considered idiomatic.In the face of the ability to write the functions explicitly and with expressive parentheses, it seems the answer is "because you can". https://nostarch.com/learn-physics-functional-programming |