Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Copying vs. sharing in functional languages (danghica.blogspot.com)
92 points by matt_d on March 11, 2018 | hide | past | favorite | 43 comments


My observation is that functional languages which are REPL-focused (such as Haskell and Mathematica) generally empower the author to well understand/test/"debug" smaller pieces of her code. In contrast, in traditional non-REPL languages such as Java and C(++), this requires writing special testing code within the actual code.

Similarly, at least in imperative languages, I made the experience that people who develop with a debugger write less testable code then people who do the debugging in their code, for instance with assertions or test frameworks. Sometimes I feel these two groups are disjoint.


It’s weird because these are totally different concepts, and catch different things: the debugger is about inspecting how your code executes, assertions allow you to detect when invariants are broken (and provide points that need inspection in the debugger), tests ensure that your code doesn’t break by accident, etc...good developers should be, no, they probably are using all three tools. That functional languages sometimes lack decent debuggers says more about community investment choices than need (e.g. Lisp machines actually had the best debuggers ever).

Repls can only give you feedback one code line at a time (and if you type a procedure/function into a REPL, you get no feedback at all until it’s called in the main loop), it’s ironic they are found more commonly in in expression-based rather than statement-based language.


I've worked a lot of high growth companies. Not a single one had test coverage requirements at the start of my employment there.

Three that bolted on test coverage requirements at some point did so after the same catalyst. An unharmonious developer joining the team and subjects old, stable code to his opinions. "JQuery is the devil!" The developer cries, as he slashes and burns all the script tags into dust.

From there, he installs <his opinion> into the toolchain and spends a tireless 5 hours getting things to sort of work. "That's so much better." he remarks to no one as he scissor claps the dust off his hands.

The next day, users wake to a world in tatters. -- they submit tickets, pleas to an angry god to return the serenity of the benign that existed a mere day ago.

Rarely at this stage is time simply rewound - though it would be easy to do, someone will remark once. The conquerer of the old tech campaigns hard for hearts and minds. He's baffled that he is not greeted as a liberator!

"Spaces are superior to tabs, the fact that the code broke after I ran a regex replace is mostly python's fault. Stack overflow said that people who prefer spaces earn more so it's basically a good business decision to move on from here."

For some reason it's easier to be righteously indignant about destruction than maintenance. The strength and intensity of the usurper tends to triumph. Besides, client side validation should be easy to fix -- and there it is. The rest of the pantheon has been cowed. Cowed by the illusion of a few softball one point tickets ahead of them.

Unfortunately, glass shelled ecosystems created amidst the layers of code sediment don't fair well against the tremors of sed. Each softball one point ticket begets two 3 pointers, and so it snowballs.

When the unharmonious one is summoned by the pantheon to help fight back the cascade, he almost always bravely calls for a complete rewrite. "This is due to the fragility of the legacy system." he always reasons.

This is about the time that the descent into madness is halted by a call for test coverage. "We've experienced a lot of regressions recently, we should probably start writing tests."

This is also about the time our regicide leaves for another company.


This is a very well-written, normalized anecdote (serious). I hope you have or start a blog, and add it into your profile at some point down the road.


Funny piece :)

I'm getting the impression the underlying message is that in most common situations a high test coverage is often overrated? This would also be my impression after working on dozens of projects


Ghci has a built in debugger with the expected features (breakpoints, single step, inspect local variables, tracing execution and time travel) but it isn't very useful because haskell is lazy. Execution can just pinball around seemingly at random making it hard to figure out what is happening.

It is possible to go lower level and just throw gdb at it but a lot of compiled functional code depends on multiple source locations and dwarf can't represent that.

On the other hand, for pure code the distinction between repl, debugger and test suite kinda blur anyway.


the irony resolved into something simpler. statement languages are about state - the fundamental primitive is about mutating a state, so it make sense that the preferred debug tool is going to let you inspect the state of the program. Expression oriented languages really encourage and/or force you to write stateless code, you don’t need to pause a program and inspect its state if you code it as an expression because it has none. that’s the theory at least, i am a clojure and clojurescript developer and i spend enough time in my debuggers (probably because real programs do in fact have state) but not nearly so much as i did in java and javascript.


To be fair, repls can do much more than one line at a time. You just need a more capable editor than a line oriented prompt.

This is not to detract from your point. I have gone further and lamented the death of debuggers. Primarily because I have seen them used so rarely in the wild. That combined with the somewhat ironic rise of functional idioms in java that are actually difficult to debug. (My understanding from last time I asserted this, is just that I don't see them used much.)


My experience is that when the debuggers are bad, they aren't used much, and when they are good, they are used all the time. The web browsers offer poor debugging experiences ATM, so for Javascript they aren't as useful as say debugging C# in Visual Studio or even Java in Eclipse.

I agree that functional programming idioms in practice makes debugging harder, but only because debuggers lack the right UI to deal with the buried control flow of things like functional orgami. I've heard F# with its Visual Studio plugin is doing better in this regard.


My apologies, my point in "functional being harder to debug," was purely on some of the new idioms in Java.

JavaScript has the advantage of the debugger keyword. Has the disadvantage of the execution engine not being hooked up to the editor for most folks. I have seen some changes to that. And I am still enamored with skewer mode. (Haven't used it much.)

Java debugging blows my mind. It was trivial to do and I recall tons of people doing it in school. I have yet to see anyone at the last few offices I've worked in do it.

Worse, many common designs and practices make it much more difficult than it needs to be.


Uh, what makes you say web browsers offer poor debugging experiences? I'd put them about on par with Visual Studio's C# debugging, even better in some areas(Immediate Mode in VS breaks down often, the debugger craps out on some LINQ-to-X expressions, etc, whereas web browsers offer all the malleability of JS right there at the debug breakpoint).


Chrome and Firefox devtools are very very behind in modern debug features, it’s like programming in the early 90s again! Not just that, but debugging is also extremely fragile, crashing often on small programs for no good reason. It can’t even call tostring automatically in the watch windows because JavaScript VMs have no easy way of quickly sandboxing those fine grained calls.


I'm not sure I understood the toString() issue you described here, and I don't recall encounting crashes with any particular regularity. What I do recall is having to deal with VS2017's inability to execute or watch many LINQ/lambda expressions which made certain debug sessions completely useless.

The biggest issues I've had with web browser debuggers is the interaction(or lack thereof) between source maps and debugging. I don't know any of the modern debug features that the browsers are missing - could you mention some?


My guess is they are referring to Reverse Debugging[1] and similar features. Basically, being able to step backwards in the code.

A feature not many realize exists already is "Restart Frame." I confess I always forget it exists in Chrome. It can get you close to "step backwards."

I don't know if you can use it to redefine a function and start it over, though. (Something you can do in languages people love the debuggers of.)

[1] http://sourceware.org/gdb/wiki/ReverseDebug


Debuggers, REPLs, assertions, tests and REPLs all perform slightly different functions and have different strengths but they are somewhat fungible.

E.g. Linus Torvalds doesn't use a debugger whereas John Carmack relies heavily upon a debugger.


OS programming makes it hard to mount a decent debugger directly. You are often stuck with interrupts and serial port interfaces, it isn't the same as writing an application. When I was young, I interned for the OS/2 device driver team, really a different experience.

I'm sure Linus at least uses a kernel debugger. But looking at https://en.wikipedia.org/wiki/Kernel_debugger, I guess not.


Lisp isn't non-strict by default, though, right? I always thought that it was pervasive non-strictness that made it painful for some languages.


My understanding is macros can make debugging in lisp painful.

It has the major advantage of the system evaluating the code is easily understood when stepping around, though.


Smalltalkers, who pretty much invented unit testing, do most of their programming in the debugger. The technique i was taught was to write a test, run it in the debugger, then when it fails at a missing method, the debugger stops, and you write the code to make the test pass, then restart the debugger.


I've tried playing around with Pharo but I never could figure out how to practice that development model of using tests and depending on the debugger. Is there a tutorial you would recommend that focused on that tactic?


"TDD in Smalltalk"

https://www.youtube.com/watch?v=pkjFTdq2q2g

It makes use of Cincom.


Well that explains the genesis of Red Green Refactor.


Whether the average developer uses a debugger or not is much more correlated with the tools at hand than anything else.

People who unit test or don't is more correlated with age and commercial experience / maturity level. Although those correlation curves aren't monotonic either.


I spend a lot of my time helping people get unstuck and I notice patterns.

People who don’t use the tools seem to trust their own eyeballs more than they trust code. Even as someone who spots bits of code that nobody else sees, I don’t trust my eyes that much. It’s not a matter of if I can spot things, because I can, but it’s not a reliable skill.

It’s a matter of reliability and a matter of saving that energy for the next step.


I'd agree with you, but with a caveat: that the degree to which you trust tools vs your eyes depends on the quality of your tools.

I, personally, don't trust source-level debugging with gdb, for example.


This article brings up an interesting question. Since much of programming is based on simply making something easier for humans to read, create and design with, is there a trade off with functional programming in regards to debugging?

I really appreciate some aspects of both styles, but I am not a die hard either way, my limited experience with functional programming has never lead me into the depths of debugging like discussed in the article. So the article actually makes me wary of going further.

I would liken this effect to a form of technical debt similar to a framework. Upfront development is faster, but when there's a problem, it's much harder to fix or work around, and problems can be more obscure.

Is this concern valid?


I've found that it really varies based on the language and the tooling it offers. I code Erlang and Elixir on a daily basis, and I've never used such a full-featured debugging environment before. I can trace messages across a distributed system, connect to production nodes to set breakpoints or tracepoints, and I can render a live visualization of all the supervision trees in my application and watch as processes spawn and are killed. And of all that is just scratching the surface of what you can do.

I also remember Racket having pretty powerful debugging capabilities, including the ability to step through programs and see how each expression is evaluated.

Some languages have worse tooling than others, but I don't think it's a problem endemic to functional programming.


Functional programming actually opens up more opportunities for debugging, as we are better able to both reason about and transform programs. The caveat is that often we need to build domain-specific debuggers, to effectively debug our high-level domain-specific code. Even in imperative languages like C++ and Java, the standard word-at-a-time debugger gets increasingly less useful as abstractions are piled up. Multiple threads really throw a spanner in the works.

Here's an example functional debugger that can step forwards and backwards in time, as well as modify code at any time:

http://debug.elm-lang.org


Backward step debuggers aren’t limited to just functional languages, they’ve been available for imperative languages as well (e.g. via gdb).


That's quite a bold promise, when in theory any statement could "launch the missiles". Just like software transactional memory, this idea, AFAICT, really needs pure functional programming to work well.


No way, I’ve implemented a few of them myself (my research area is in love programming), and none of the languages I’ve done were functional. All you need to do is checkpoint, which is true for non trivial FP programs as well.


Checkpointing is more fine-grained and yet, essentially free in pure functional languages though. The tooling has got to be simpler for that reason alone.


You should no better than that. It is only free in pure functional languages if they keep every single world around, then the overhead of checkpointing is nil because they are basically doing it already! So choose between "save the world because that's what we already do" or "save the world after the fact," it is pretty much the same.


> You should no better than that. It is only free in pure functional languages if they keep every single world around

Which a debugger for a purely functional language would do for you. Which is why I said it would be almost trivial to create this kind of tooling in such a language.

> So choose between "save the world because that's what we already do" or "save the world after the fact," it is pretty much the same.

Not sure I agree. Save the world after the fact would seem to require much deeper integration with the abstract machine to capture the evaluation context. I was merely pointing out that this integration is shallow in a purely functional language, and such a tool would be easy to create.


> Which a debugger for a purely functional language would do for you.

Yes, you are already checkpointing if you have such an interpreter, not that I think many programs would actually qualify.

> I was merely pointing out that this integration is shallow in a purely functional language, and such a tool would be easy to create.

Most production language reverse debugging techniques are similarly shallow because they work mostly at the OS level (GDB), it isn't hard to capture evaluation context when you capture everything.

How is reverse debugging working out for Haskell then? I assume, since it is trivial over there, it must be common?


> How is reverse debugging working out for Haskell then?

The Elm debugger I posted above is a perfect example. Unlike your GDB example, it doesn't only work for a subset of x86 instructions or require monumental effort to implement. It's a high-level domain-specific debugger, that was simple to specify and build. For those of us that want to write high-level code with many layered abstractions, working one-word-at-a-time using GDB is simply not a great way to debug, even if it does now sometimes work backwards. We want to easily build our own debuggers.


http://rr-project.org/ does it by recording the execution. If the bug occurs rarely, you can keep retrying your recordings until you get one that exhibits the bug, and then you can step forwards and backwards through it at your leisure.


They build up a persistent (i.e., functional) structure, under the hood, so that stepping back and forward again doesn't redo the actions.


"and yet, it works"


> is there a trade off with functional programming in regards to debugging?

I have looked and large and hairy C++/Java/C#/Erlang/Python programs. I think overall Erlang are the easiest to debug. Two reasons are: state is most likely to be passed in explicitly, and data is immutable.

Compare to, say, looking at a class instance in an OO language, which inherited from a bunch of other classes, and then had some methods called on it, modifying its internal state, and now you're and asking yourself "hmm, this seems broken, but how did we even get to this point?". vs looking at function like "update_state(State, NewData, ...) -> ... NewState.". In the second case it is clear what is happening, what the old state was, what the new is. If the bug happens on a live system, it is easy to attach a trace that would trigger only when NewData matches something or old state matches, or both match.


There are perfectly good debuggers for functional languages, but honestly, it's not as useful as it is for imperative languages in my experience. More useful is profiling information -- both execution and heap.


Hello all, I am the author of the blog post. Now the tool is available online: https://fyp.jackhughesweb.com/tasks/

Please fill out the feedback form if you have time, so we can improve the project.


I fail to see the point of this work. When I program, what I want to see is how each statement relates a precondition to a postcondition. From this point of view, the selling point of value-oriented programming is that conditions are partially reified in the program as abstract types, rather than merely existing in the programmer's head or a separate piece of paper. Note that this does not require the complete absence of mutability: an abstract value may contain the identities of mutable objects, but the role of abstraction is to constrain the ways in which these objects can in fact mutate. Laziness is a prime example of this.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: