Live data from Hacker News

Tacit programming

en.wikipedia.org

51–60 of 95 posts

Re: Tacit programming

#51

Tinkering with APL (Dyalog) gave me one of my most mind-bending programming moments. dismal ← 10⊥(⌈/10⊥⍣¯1⊢) This is the complete solution to addition in the framework of Dismal Arithmetic [1]. The pivotal idea there was the inverse of a function, and "trains". Until that moment of insight, I was fiddling about with dfns, which looks janky in comparison. dismal ← {10(⊤⍣¯1)⍵}∘{⌈/⍵}∘{10(⊥⍣¯1)⍵}⊢ ⍣¯1 is APL for "inverse…

I can't seem to figure out what you mean by inverse. Is the inversion of addition subtraction? Neither post seems to explain what it is (or maybe it assumes knowledge of ⊥ and ⊤?)

Given a function F that accepts an argument x and returns y, its inverse is another function which, when passed y will return x.

For dyadic functions, the incerse argument is always the right side one.

So negation is its own inverse.

So the inverse of 2+ is indeed subtraction of 2.

Re: Tacit programming

#52
Oh, I’m huge on doing the opposite of this.

When I write JS (not TS), I prefer as much as possible to use destructuring in every function definition. Named keyword arguments.

Try to change the name of something as little as possible even as it gets passed around. It’s more powerful than a type system in some ways. Not always doable, but just try it sometime; it’s fun.

I call it nominative programming.

Re: Tacit programming

#53

The wiki page leaves out the source of the the 'point-free' nomenclature - category theory ( e.g. https://en.wikipedia.org/wiki/Pointless_topology ). The original game was talking about sets (or other similar objects) without talking about 'set membership'/'elements', whence 'point-free'; you want to only talk about functions between sets (or morphisms between objects), and build everything up from those. Is 'tacit'…

The name "tacit" comes from the APL family as far as I know. It certainly fits with Iverson's style, as he was fond of seeking out just the right word to describe something regardless of obscurity ("ravel", "copula", etc.). I think the name would have come about after the development of function trains in 1988, and I found a paper "Tacit definition" about Iverson's J from 1991: https://dl.acm.org/doi/10.1145/114054.1…

I was inspired to write up a little section on Iverson's approach to naming (including his probable coinage of "bubble sort"): https://aplwiki.com/wiki/Ken_Iverson#Naming_things

Re: Tacit programming

#54
post #9

Absolutely every single time when I use functional programming, I store my intermediary calls in a variable, specifically because naming that variable forces me to explain what that intermediary result should be. If the intermediary result makes no sense, and only the function composition makes sense, I'll create a new well named function that does the chaining, even if it's single use. This is literally the only way…

Yeah, point-free sounds cool, until you actually try it out. Even in their example they are not point-free: compose(foo, bar, baz) Here compose is applied to three "points" (which happen to be functions).

Point-free means that you don't bind variables, so this particular example is indeed point-free.

Re: Tacit programming

#55
post #4

Why not translate your code to pointfree style automatically? Using[0], you can go from quad a b c = let d = b * b - 4 * a * c in ((-b + sqrt d) / 2 * a, (-b - sqrt d) / 2 * a) to ghci> import Control.Monad ghci> quad = ap (ap . ((.) .) . ap (ap . (liftM2 (,) .) . flip (flip . ((*) .) . flip flip 2 . ((/) .) . (. sqrt) . (+) . negate)) (flip (flip . ((*) .) . flip flip 2 . ((/) .) . (. sqrt) . (-) . negate))) (flip (…

Apart from "just because we can" or "it's fun", why on earth would someone prefer the second style?

In this example I can’t imagine anyone preferring the second style, but there are cases where it’s nicer. For example compare the tacit:

    foo = h . g . f
With the more verbose:

    foo x =
      let
        a = f x
        b = g a
        c = h b
      in c
If a, b, and c have useful names that help you understand the code then the second function might be preferable- but in a lot of cases all the intermediate variables are just adding noise and making it harder to see what’s happening at a glance. The tacit example makes it very clear at a quick glance exactly what’s happening.

My personal rule of thumb is that if you are passing combinators in as arguments to other combinators then you should probably stop, but straightforward chaining is usually okay.

Re: Tacit programming

#56
post #29

Earlier quoted context omitted.

> You have to jump at a billion functions definitions before you can hope to understand what a program is doing On the contrary, if you assume that your functions are good abstractions then you shouldn’t need to know their implementation details in order to compose them. You can tell what it does by looking at just what you have in front of you. If that’s not the case, then you’re not looking at a good example of thi…

> > You have to jump at a billion functions definitions before you can hope to understand what a program is doing > On the contrary, if you assume that your functions are good abstractions then you shouldn’t need to know their implementation details in order to compose them. You can tell what it does by looking at just what you have in front of you. In maintenance, you often cannot assume that your functions are good…

> One of them is doing something wrong, or at least something that needs changed. Which one?

While this is as true in FP as it is anywhere, my experience is that it’s rarely true of the kind of small pure functions that tend to be composed like this. When someone is using a chain of functions like this, they usually are good abstractions that don’t change, and are short enough to be trivially correct.

Re: Tacit programming

#57
post #29

Earlier quoted context omitted.

> You have to jump at a billion functions definitions before you can hope to understand what a program is doing On the contrary, if you assume that your functions are good abstractions then you shouldn’t need to know their implementation details in order to compose them. You can tell what it does by looking at just what you have in front of you. If that’s not the case, then you’re not looking at a good example of thi…

> > You have to jump at a billion functions definitions before you can hope to understand what a program is doing > On the contrary, if you assume that your functions are good abstractions then you shouldn’t need to know their implementation details in order to compose them. You can tell what it does by looking at just what you have in front of you. In maintenance, you often cannot assume that your functions are good…

Point free when done with _andThen_ instead of _compose_ isn't that much different from reading basic statement oriented programs delimited by semicolons:

    (
      f andThen 
      g
    )(x)
vs a = f(x); g(a);

_andThen_ is of course just compose with the arguments flipped such that you can compose from left to right instead of right to left.

In FP languages, and languages that use FP combinators, it is usually more efficient to use composed functions with combinators that do iteration and copying like _map(list, f):List_, because the iteration and copying happens during each map application:

    map(map(l, f), g)

and

   map(l, f andThen g)
produce equivalent results, but the second is faster and has fewer allocations.

For determining the arguments to f and g and map, typed fp languages usually have ide features which can show you the inferred type of each expression, or you can jump to the definition and see it, usually by hovering or key chord while the cursor is over it.

    map[A, B](
      fa: List[A], 
      f: A => B
    ): List[B]
and etc.

This allows you to read things easily and avoid cluttering the code with types where they can be easily inferred.

Point-free style is important to make the usage of such combinators acceptable but experienced devs do extract and name a composition when it becomes difficult to understand. Of course, the treatment of functions as effect-free black box transformers for equational reasoning also makes these refactoring extractions to variables safe to do.

You get a feeling for what is too much over time, and settle on when to use point free style and when not to.

On efficiency - good compilers can often identify nested/chained map applications and rewrite them as a single map application during compilation, but map fusion isn't guaranteed by all compilers or all _map_ instances. The evaluation strategy (lazy/eager) of the language and/data structure also plays a role in whether or not map fusion with point-free style is more efficient or optimisable.

Re: Tacit programming

#58
post #9

Absolutely every single time when I use functional programming, I store my intermediary calls in a variable, specifically because naming that variable forces me to explain what that intermediary result should be. If the intermediary result makes no sense, and only the function composition makes sense, I'll create a new well named function that does the chaining, even if it's single use. This is literally the only way…

I've been using tacit programming intensively to the extent I get rid of most variables. I use both syntactic threading macros and functional combinators to achieve this. It is a double-edged sword in that it can make code as ugly as the original code it is trying to improve upon, but working without variables isn't that difficult nor does it lead me to intense clusterfucks. Composing lambdas contribute to this a lot more though.

As for making things clearer for everyone, well, this is code I work on solo (hobby), but I think providing example inputs as well as debugging macros can help a lot. Consider the following:

    (defn parse-it [dependencies]
      (pp->> dependencies
             str
             str/split-lines
             (keep (|| re-matches #"- (\d+(?:\.\d+)?) -> \[((?:\d+(?:\.\d+)?(?:, ?)?)+)\]"))
             (>>- (map-> (juxt-> second
                                 (->> third (re-seq #"(?:\d+(?:\.\d+)?)")))))))
    
    (parse-it (str "- 4 -> [1, 2, 3]\n"
                   "- 5 -> [4, 2]\n"
                   "- 6 -> [1, 2, 5]"))
    
    ;; The use of pp->> will lead to this getting printed
    ;; ->> dependencies                              : "- 4 -> [1, 2, 3]
    ;;                                                  - 5 -> [4, 2]
    ;;                                                  - 6 -> [1, 2, 5]"
    ;;     str/split-lines                           : ["- 4 -> [1, 2, 3]"
    ;;                                                  "- 5 -> [4, 2]"
    ;;                                                  "- 6 -> [1, 2, 5]"]
    ;;     (keep (|| re-matches #"- (\d+(?:\.\d+)... : (["- 4 -> [1, 2, 3]" "4" "1, 2, 3"]
    ;;                                                  ["- 5 -> [4, 2]" "5" "4, 2"]
    ;;                                                  ["- 6 -> [1, 2, 5]" "6" "1, 2, 5"])
    ;;     (>>- (map-> (juxt-> second (->> third ... : (("4" ("1" "2" "3"))
    ;;                                                  ("5" ("4" "2"))
    ;;                                                  ("6" ("1" "2" "5")))
In the end I think it doesn't really bring a lot, but it's especially useful in making short piece of code more readable:

    (->> [1 2 3 4]
         (map (when| odd? inc)))
    ;; vs
    (->> [1 2 3 4]
         (map (fn [x]
                (if (odd? x)
                  (inc x)
                  x))))
    ;; result (2 2 4 4)
Or this:

    (-> k-or-ks (when-not-> coll? list)
        (map-> ...do-something))
    ;;vs
    (let [ks (if (coll? k-or-ks)
               k-or-ks
               (list k-or-ks))]
      (map ...do-something
           ks))
Or even this:

    (-> 1 (juxtm-> :incd inc :decd dec))
    ;; vs
    (let [n    1]
      {:incd (inc n)
       :decd (dec n)})
Granted I have insane shits like this teleport arrow (very useful though):

    (-> '(1 2)
        (•- (conj (-• first dec)))) ;; => (0 1 2)
Or this >-args "fletching"

    (-> {:a 1 :b 2}
        (•- (-> (>-args (-> (/ (-> :a) (-> :b))))
                (->> (assoc (-•) :result))))) ;; => {:a 1, :b 2, :result 1/2}
I actually write this kind of stuff in my code ahahaha (the right move is to implement assoc-> of course hahahaha). Now there are combinators I wrote I never use, like departializers, unappliers, argument shifters, etc

Re: Tacit programming

#59

I love the concept of point-free programming - write your function by simply concatenating the transformations you want. I just hate reading the resulting code written by others. What information is expected to come in, and exactly what data passes from one step to the next, and in what position? Data type signatures only go so far. Point-free means you have all that wiring in your head, without assistance from the n…

I'm smiling a bit because I know exactly what you mean and generally agree, with an exception. A common idiom in Elixir is to return {:ok, result} or {:err, :reason} from calls that can fail. Leaning on that idiom, good function names, and good errors goes a long way: params[:id] |> find_user |> verify_user_has_access_to(params[:post_id]) |> etc When I define the functions I'll use pattern matching on the first argum…

I am not sure about Elexir, but in Ocaml, someone using this style would gravitate towards Result.map and Result.bind (Defined like this: given (Ok a) or (Err b),

- (Result.map f) returns (Ok (f(a))) or (Err b)

- (Result.bind f) returns f(a) or (Err b)

)

One could then write

    params.id
    |> find_user 
    |> Result.bind (verify_user_has_access_to params.post_id)
    |> etc
Reasons for that are 2-fold

1. It de-clutters your functions (you do not need that match statement anymore)

2. It becomes evident

  - which functions will simply pass down errors (bind or map) vs. which ones may handle them

  - which functions may raise new errors (bind can, map can't)

Re: Tacit programming

#60

Tinkering with APL (Dyalog) gave me one of my most mind-bending programming moments. dismal ← 10⊥(⌈/10⊥⍣¯1⊢) This is the complete solution to addition in the framework of Dismal Arithmetic [1]. The pivotal idea there was the inverse of a function, and "trains". Until that moment of insight, I was fiddling about with dfns, which looks janky in comparison. dismal ← {10(⊤⍣¯1)⍵}∘{⌈/⍵}∘{10(⊥⍣¯1)⍵}⊢ ⍣¯1 is APL for "inverse…

Interesting, never heart about dismal arithmetic before. Squint hard enough and it starts to look like tropical geometry. This is a different construction where you give the addition and multiplication operators in a polynomial a different meaning. I've been trying to have a better understand why this is useful. I know tropical geometry has been used to improve things like price discovery, but I never got to a better understanding as to why. I At any rate I would be curious to know what dismal arithmetic can do for me.
Post reply on HN