Hacker News new | ask | show | jobs
by indigosun 810 days ago
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

1 comments

Wow what a lot of parentheses! To me the first example is perfectly clear because it follows the same precedence rules I learned for arithmetic at school, along with most programming languages. There's nothing special about Haskell here, in Python I would write:

    >>> math.sqrt(2) + 1 * 3 + 3 * 2 + 1 / 7  
    10.557070705230238
The only difference I see is the parentheses used for function application in python.
You should see when negative numbers are involved.

Due to Haskell's rigor in regard negative numbers:

    ghci> -1 + 2
    1
    ghci> 2 + -1
    <interactive>:12:1: error:
    Precedence parsing error
        cannot mix ‘+’ [infixl 6] and prefix `-' [infixl 6] in the same infix expression
    ghci> 2 + (-1)
    1
    ghci> -1 * 3
    -3
    ghci> 3^-1
    <interactive>:3:2: error:
    • Variable not in scope: (^-) :: t0 -> t1 -> t
    • Perhaps you meant one of these:
        ‘^’ (imported from Prelude), ‘-’ (imported from Prelude),
        ‘^^’ (imported from Prelude)
    ghci> 3^^-1
    <interactive>:4:2: error:
    • Variable not in scope: (^^-) :: t0 -> t1 -> t
    • Perhaps you meant ‘^^’ (imported from Prelude)
    ghci> 3**-1
    <interactive>:5:2: error:
    • Variable not in scope: (**-) :: t0 -> t1 -> t
    • Perhaps you meant ‘**’ (imported from Prelude)
    ghci> 3**(-1)
    0.3333333333333333
    
    ghci> sqrt 2 + 1 * 3 + 3 * 2 + 1 / 7
    10.557070705230238
    ghci> sqrt $ 2  + 1 * 3 + 3 * 2 + 1 / 7
    3.3380918415851206
    ghci> -sqrt 2  + 1 * 3 + 3 * 2 + 1 / 7
    7.728643580484048
    ghci> -sqrt $ 2  + 1 * 3 + 3 * 2 + 1 / 7
    <interactive>:25:1: error:
    • Non type-variable argument in the constraint: Num (a -> a)
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall {a}. (Floating a, Num (a -> a)) => a

    ghci> sin -1
    <interactive>:7:1: error:
    • Non type-variable argument in the constraint: Num (a -> a)
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall {a}. (Floating a, Num (a -> a)) => a -> a
    ghci> (sin -1)
    <interactive>:10:1: error:
    • Non type-variable argument in the constraint: Num (a -> a)
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall {a}. (Floating a, Num (a -> a)) => a -> a
    ghci> sin (-1)  -- -0.8414709848078965
Let's see,

    ghci> (sin (-1))  -- -0.8414709848078965
Ahh, now that's better.