(no title)
kamray23 | 1 year ago
However as I mentioned, since it is not true that most people can read FP code, I mostly avoid using it. The example comes from my solution to AoC2023's Day 3, "Gear Ratios", which is just about the only thing I use Haskell for.
That doesn't mean that using it doesn't have practical applications, since being used to multiple paradigms opens you up to unconventional solutions. I've recently sped up a MATLAB function ~100x through using a more functional style to manipulate memory more efficiently. Async/await, certain styles of modern iterator manipulation and generators escaped F#, CLU and others into C# and from there into the world at large specifically because Microsoft programmers saw a problem they had had a solution for in previous functional projects. So it's not all useless.
For the record, a more imperative version could be written as
symbols = [ imagine there's stuff here ]
gears = []
for (symbol, coord) in symbols:
if symbol == '*':
ns = neighbouringNumbers(coord)
if len(ns) == 2:
gears.append(ns)
or in Python's functional-inspired notation which directly mirrors what's happening in the Haskell code gears = [ neighbouringNumbers(coord)
for symbol,coord in symbols
if symbol == '*' and len(neighbouringNumbers(coord)) == 2 ]
though that requires an unnecessary extra call to neighbouringNumbers, which you could solve with a walrus op but I can't remember how to do that. I also changed the entire pair being passed to neighbouringNumbers (which was convenient in Haskell) to only the coordinate that is required (which is convenient in Python).Personally I just find nowadays that having to comprehend "it collects neighbour-pairs from '*' symbols" from the imperative code harder than having that be the thing that is actually written down.
skybrian|1 year ago
For example, if you’re looking at Lisp code and you don’t know whether an outer term is a function, macro, or special form, you really don’t understand anything inside it except at the most superficial level. It might as well be JSON. Macros aren’t marked, so any unknown term might be a macro. (Knowing the surface syntax is still helpful, though. Well-known syntaxes for data are useful.)
With Forth it’s pretty bad, too. Not knowing what a single word does means that you don’t know what’s on the stack afterwards.
A Unix pipeline is a bit more orderly since you know that there are streams of bytes. You know the command names and the arguments to each command. But if you don’t know what a command does, you don’t know much at all about what the data looks like after that point.
I find currying and point-free programming to be pretty opaque because I can’t tell where the function calls are or how many arguments each function takes from usage. I don’t know what the dataflow looks like. It seems like you need to know some precedence rules too?
Languages with conventional function call syntax, augmented with named parameters, seem better. I can tell where the function calls are and where the inline functions are. Augmented with reasonable names for temporaries, I can make reasonable guesses about what the code does.
These syntax concerns seem independent of whether it’s a functional or imperative language? Making reasonable guesses is what we do when we read pseudocode. Maybe what I’m saying is that some syntaxes seem better for pseudocode than others, and I like languages that look like pseudocode better.
I suspect that these syntax differences also have an effect on how good the error messages are when you screw up.
kazinator|1 year ago
This is true, but:
- the name of the operator is typically a word that you can easily search for in the documentation or on the web, or with "jump to definition" in your editor, if it's something locally defined in the source tree.
- you usually understand the shape of what is inside it. You know what parts of the program you are looking at are arguments to that mysterious operator and which are not. If asked which expression is the third argument of that operator, you can easily find it and know where it begins and ends.
lispm|1 year ago
Yes, that's a problem.
There are two clues:
* Macros typically have naming conventions. For example anything beginning with DEF should be a defining macro, not a function. Anything with DO- will be a control structure. Anything with WITH- will be a scoping macro. And so on. Avoid active names like CREATE- and use DEFINE- instead. CREATE-CLASS would be a function, DEFINE-CLASS would be a macro.
* In source code the enclosed code typically has syntax and special indentation, when used with macros. Functions have certain uniform indentation rules.
Above is a function and uses one of the typical formatting/indentation rules for functions. Line up the arguments. Start with the required argument and then the named argument pairs (name, arg).The macro looks different. The first important things like name and superclasses are on the first line. The other parts of the class specification follow and are indented by two characters.
Developers who write macros should make it clear what a macro is, by using hints like these.kerkeslager|1 year ago
I write Python a great deal for a living these days and the Python code isn't much clearer to me. In both the Python and Haskell examples I can tell what it's doing (except the opaque neighbouringNumbers)--I just can't tell why it's doing it.