4 ms·
One of the things I really like about this approach is how well it lends itself to separating what needs unit testing from what is unsuitable to unit testing an
by bcbrown 8y ago
One of the things I really like about this approach is how well it lends itself to separating what needs unit testing from what is unsuitable to unit testing and should instead be validated by higher-level end-to-end/functional testing.
I think it's also related to the concept of building a DSL in which to implement business requirements. Once you have the right 'primitives' you can then combine them in useful ways that are easy to verify (by reading the code) that the implementation matches the requirements.
- pwm 8y agoExactly. Also once all what you have in core are types and functions then things like mocking becomes obsolete. You can just instantiate those types for real.
- daenz 8y agoBingo. My philosophy has started to become "If it can be functional without sub-optimal overhead or confusing state magic, it probably should be." Delaying that "DSL" layer as long as possible keeps your system super straightforward and easily testable.
- MichaelMoser123 8y agoHow do you deal with performance problems? I mean to add an element to a list you need to create a copy of the list and then add the new element, all in order to remain functional. Isn't that very expensive?
- adrianN 8y agoPurely functional data structures are more efficient than the naive "just copy everything" approach. You can do anything that's possible in a stateful imperative setting with at most a logarithmic slowdown.
- MichaelMoser123 8y agoLet's say the entries of the original list and the new one point to the same objects, still for a list of a thousand entries you need to copy all the link entries to add one on top of it. What am I missing?
- kryptiskt 8y agoIf you just want to insert an element at the beginning of the list you can use the original list as the tail of the new list. It's immutable, so it's not going to change under you. If you want to insert stuff randomly, you wouldn't use lists to get the best results in a functional setting. You might use something like finger trees instead.
- etatoby 8y agoIf you are working in an immutable context, meaning that you are building immutable data structures to contain immutable values, there are many useful structures that you can use. For instance, the "single linked list" or simply "list" has constant-time push and pop operations. Here such a list of 3 elements (NIL is a special value that says "no more list") L = (((v1, (v2, (v3, NIL))) To add v0 in front, you just create a new "cell" that reuses the old list (which remains valid by itself) as the "tail" of the new list: L2 = (v0, L) L2 = (v0, (((v1, (v2, (v3, NIL)))) Depending on your requirements, there are more complicated and smarter data structures that can make the operations you care about either constant, or at most logarithmic. I recommend this free book to learn more and get better at Computer Science in general: https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book.html https://mitpress.mit.edu/sites/default/files/sicp/full-text/...
- MichaelMoser123 8y agoI did scheme assignments as part of the programming language course back in the early nineties - but then I was wondering why the runtime environment had so many GC pauses (it was some A* search assignment, if I remember correctly) also it wasn't quite fast by any standards
- 8y ago
- gary_bernhardt 8y agoYou only have to copy if your array is implemented as a simple linear sequence of memory addresses. More advanced implementations don't have to copy everything. E.g., Clojure's vectors (its array equivalent) are effectively O(1) for the common array operations that are O(1) on naive arrays, like indexing and insertion. But Clojure vectors are still purely functional. (The actual time for some of those ops is O(log32(n)), but log32(1,000,000) = 4, so it's effectively O(1).) The term for this is "persistent data structures", usually implemented via trees, where replacing an object in a vector is implemented by building a new tree, reusing all of the old nodes except the ones that appear in the path from the root to the replaced node. That's why Clojure's Vector is log32; it's a 32 b-tree. (I'm writing this from memory and have little Clojure experience, but I'm pretty sure I have it right.) Many languages have implementations now, but most aren't as fast as Clojure's. E.g., there's immutable.js: http://facebook.github.io/immutable-js/ http://facebook.github.io/immutable-js/
- twtw 8y agoFor people who are interested in these things, I'd highly recommend Chris Okasaki's thesis/book on the topic. I was not familiar at all with this stuff when I read through it the first time, so it was a tad mind-bending and I probably understood ~10% of it, but it was certainly educational. http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf
- bebop 8y agoYou can also use what is called a transient in Clojure, where you can get an mutable copy, do what you need to do, then return a immutable copy back. The biggest benefit seems to be when adding elements within a for loop. The example on this page illustrates how you could use this. https://clojure.org/reference/transients#_example https://clojure.org/reference/transients#_example
- btschaegg 8y agoStrongly agree. Even more, it's also a gread heuristic to use if you want to refactor existing code into something more testable. At times, the simple process of figuring out what the really necessary points of mutation/IO are and how to "fence them off" is all I need to simplify big chunks of an existing "ball of mud".