Live data from Hacker News

Tacit programming

en.wikipedia.org

61–70 of 95 posts

Re: Tacit programming

#61
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…

Doing this for every intermediate result badly harms readability.

Consider McIlroy's famous 1-liner:

    tr -cs A-Za-z '\n' |
    tr A-Z a-z |
    sort |
    uniq -c |
    sort -rn |
    sed ${1}q
Adding intermediate variables like "newline_delimited", "lowercased", "sorted", etc... is just pure noise. It's the equivalent of a newb programmer putting a comment over each line of simple code explaining in English what that code does, despite it being clear already.

"There are only two hard things in Computer Science: cache invalidation and naming things"

One of the great benefits of pipelines and tacit programming is the ability not to name intermediate results, especially when those results speak for themselves, are not re-used, and have no significance in isolation.

Re: Tacit programming

#62

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…

> Point-free means you have all that wiring in your head, without assistance from the notation.

I completely agree. It fatigues me to read unnecessarily point-free programming. I have to translate it into a point-ful style in my head to understand it.

For example, you could take this piece of Haskell code and make it more point-free. I think it's readable at first, if redundant (you could remove the last parameter xs, for example).

  -- map: apply the function f to each element of the list xs
  map :: (a -> b) -> [a] -> [b]
  map f xs = foldr (\x xs -> f x : xs) [] xs
Some people would prefer to write it like this:

  map :: (a -> b) -> [a] -> [b]
  map f = foldr ((:) . f) []
That lambda takes more effort to parse and think about, though, at least for me. The first version was pretty readable. Now, when I read "((:) . f)" I'm thinking "okay, so the function f takes an argument, and passes the result to the (:) function, which normally takes two parameters; with one argument, it returns another function that takes one list parameter and returns it with the result of "f x" prepended to it." And to do this, I have to know implicitly how many arguments the function (:) takes in order to parse and understand it correctly (though in this case, it's obvious, because (:) is ubiquitous).

Pointfree.io would take what I wrote and transform it into

  map = flip foldr ([]) . ((:) .)
But I'm pretty sure no one would write that. It takes even more effort to correctly parse this.

That said, I don't write any Haskell. I just used to try to, but found I didn't like it.

Re: Tacit programming

#63
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…

Well, none of that depends on your functions being point-free or you writing their parameters down.

The Haskell culture in particular has a very strong idea of naming all the things that can have a good name, and none of the ones that can't. Point-free functions are about that last part, but you only touched the first part. (Personally, I think the culture is too radical, but you can't really argue against a principle like this.)

Re: Tacit programming

#64
If this interests anyone who hasn't played with it yet, I highly recommend trying out Factor. For me, it makes programming fun again.

Some good practice problems can be found on Advent of Code, Codewars, and the Perl Weekly Challenge.

Re: Tacit programming

#65

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…

>The pivotal idea there was the inverse of a function

I was curious about this piece of unexplained cryptic code, so I did a little investigation to see what's going on. Unfortunately there aren't any revolutionary concepts here, just esoteric notation. I'll explain:

To do that "dismal addition" thing, you need to split a number into digits and build a new number using the largest digit at each position. dismal(123, 321) = 323.

APL gives you an operator to make a number out of a sequence of digits: that inverted T you see in OP's code. The left operand is the base. `inverted_T(10, [1, 2, 3]) = 123`.

It gives you another operator to split a quantity into a hierarchy of units. That's the Tee you see in their second snippet. The left operand is a sequence of radixes. An inch is 2.54 cm, a foot is 12 inches, a yard is 3 feet. To transform 130 cm into ft/yd/in, you'd do: `Tee([3, 12, 2.54], 130) = [1, 1, 3.18]`.

So, OP wanted to use this Tee operator to split a number into digits. The problem is, they don't know beforehand how many digits the number has! If it's 2 digits, they must do `Tee([10, 10], number)`. If it's 3, they must do `Tee([10, 10, 10], number)`. (Because `Tee([10, 10], 123) = [12, 3]`). So in the second snippet they tried to do some juggling to get the number of digits and use it in the Tee function (I guess).

What OP really needs is the inverse function of inverted_T. And wouldn't you know it, APL can give you the inverse of a built-in function or a sufficiently simple user function. How? Maybe an operator? No...

See that operator that looks like a puckered face? That operator applies the function to its left, as many times as the operand to its right, to whatever is to the right of the sideways T. BUT, if the right operand is negative, it applies the inverse of the left operand. Basically, the all-powerful "invert function" operation is hidden as a special case of another operator...

In sum, here's my interpretation of OP's code in pseudocode:

Using:

    encode(base, seq) =  inverted_T 
    max(a, b)         = a gamma b
    reduce(fn, seq)   =  slash 
    superapply(fn, times, seq) =  puckered  sideways_T 


    let dismal =
      encode(10, reduce(max, superapply(encode(10), -1)))
So,

    dismal([123, 321, 111])
applies the inverse of `encode(10)` one time to each sequence item, giving:

    [[1, 2, 3], [3, 2, 1], [1, 1, 1]]
Reduces using max

    max(max([1, 2, 3], [3, 2, 1]), [1, 1, 1])
giving

    [3, 2, 3]
and encodes it in base 10, giving 323.

So that's it. Nice standard library, awful syntax.

I think that OP's "epiphany" was finding a quirk in this esoteric language to counteract another quirk.

Anyway, having satisfied my curiosity, I'm going to promptly forget everything about this :)

Re: Tacit programming

#66
JavaScript is great for point-free programming! Make sure you check out Ramda.js https://ramdajs.com/

It’s fun in the sense that solving a puzzle is fun, but I avoid it for anything I need to maintain long-term.

But it’s good practice for understanding combinators which is useful for some kinds of problems.

Re: Tacit programming

#67
post #61
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…

Doing this for every intermediate result badly harms readability. Consider McIlroy's famous 1-liner: tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed ${1}q Adding intermediate variables like "newline_delimited", "lowercased", "sorted", etc... is just pure noise. It's the equivalent of a newb programmer putting a comment over each line of simple code explaining in English what that code does, despite…

While I agree that sometimes I do break my rules myself, when I don't want to spend 15 minutes to name something, I don't think your example convinced me of your case.

Would you honestly expect someone to know by heart what "sort -rn" or "uniq -c" do? This forces the reader to know what all the arguments mean by anyone reading this code.

If you'd try to push this code, I wouldn't let it pass code review without a comment on each line (except plain "sort", that one's really obvious).

Re: Tacit programming

#68
post #50
post #20

The broader idea of “pass a value between functions without naming it in the caller” crops up in a few other places outside FP. In Rust there is the “builder pattern”[0] where the builder isn’t mentioned directly: ByValueBuilder::new() .with_favorite_number(42) .with_favorite_programming_language("Rust") .build() In OO land they are called “fluent interfaces”[1], commonly used when building SQL queries while only men…

The builder pattern [0] is orthogonal to fluent interfaces. It has merely become customary for builders to have a fluent interface. The defining feature of the builder pattern is that you don’t set configurable properties on an object itself, but that you use a separate object (the builder) to set the properties on, and then have it build the final object (on which the respective properties are then usually immutable…

Just to add a bit of clarity: GP's second example isn't a builder pattern, because it's missing the actual build step. Builder patterns use method chaining to configure an object, but the final result is an object of a different class, something that doesn't have those initial methods but instead lets you use the result of what was built up. Java's StringBuilder is a classic example, which exists to avoid the O(n^2) of concat'ing multiple values sequentially, after which you use .toString() to return the actual String object built up. GP's first example is one where the builder pattern lets you name constructor arguments, and during the final build it would throw an exception if you have an incompatible combination.

Whether GP's second example counts as a fluent interface is a bit iffy just because of SQL keywords and how it works in general, but possibly a good way to think of fluent interfaces is madlibs: "Find a ___(noun) that is ___(adjective)". Instead of creating a function with complicated arguments like "find(noun, adjective)" or "find(Noun(n, adjective))", you'd mimic the sentence structure with something along the lines of "collection.find(noun).thatIs(adjective)" - the chained methods interact with each other to flexibly specify complicated arguments. The ".thatIs()" could for example be completely omitted or specified multiple times to further restrict what "noun" to return. The query builder is iffy because while it looks like one on the surface, it's actually plain method chaining where each call modifies and returns the original object without the context that's passed to the next call in a fluent interface.

Method chaining is the simple syntactic pattern that enables both of these other patterns. An umbrella term they both fall under.

Re: Tacit programming

#69
post #32

Earlier quoted context omitted.

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).

That example is as point free as it gets. Just because the syntax doesn’t look like Haskell doesn’t really change that.

You are applying functions to arguments, aren't you? So "point-free" means you cannot apply a function to arguments on the left hand side of a definition, but you are allowed to do so on the right hand side? If that's point-free, it is also point-less.

Re: Tacit programming

#70
post #67
post #61

Earlier quoted context omitted.

Doing this for every intermediate result badly harms readability. Consider McIlroy's famous 1-liner: tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed ${1}q Adding intermediate variables like "newline_delimited", "lowercased", "sorted", etc... is just pure noise. It's the equivalent of a newb programmer putting a comment over each line of simple code explaining in English what that code does, despite…

While I agree that sometimes I do break my rules myself, when I don't want to spend 15 minutes to name something, I don't think your example convinced me of your case. Would you honestly expect someone to know by heart what "sort -rn" or "uniq -c" do? This forces the reader to know what all the arguments mean by anyone reading this code. If you'd try to push this code, I wouldn't let it pass code review without a com…

> Would you honestly expect someone to know by heart what "sort -rn" or "uniq -c" do?

For people that program regularly in bash, I would. If it was a rare bash script in a code base where many team members didn't know bash well, comments would be appropriate. Even there, though, that's not the same as introducing superfluous intermediate variables.

The larger point here relates to "intended audience" or "what competencies may I assume the reader has?". This is a matter of art, not science, and highly context dependent.

Take an extreme version of the point you just made:

    const x 4*2;
"Would you honestly expect someone to know by heart that `*` in JS means multiplication?"

Clearly that is absurd, because that knowledge is an assumed competency.

What about `**` for power?

What about `^` for XOR?

What about knowing that `-~x` is equivalent to `x+1`?

Where exactly do you draw the line? You can "err on the side of over-commenting" but only so much, because taken to an extreme it hurts readability, and will annoy everyone.

Responsibilities are distributed. Is clarification the responsibility of the author or the reader? What can I assume "a reasonable reader" should know? The answer depends on many things. But the right answer isn't a blanket policy of assuming incompetence and explaining every detail with comments or intermediate variables.

Post reply on HN