A philosophical difference between Haskell and Lisp (2015)
chrisdone.com
chrisdone.com
Its appeal to me is like the appeal of math to me, not like real analysis math but abstract algebra math. The beauty, the purity of mathematics of younger days that once became lost after encountering the sad complexities of the world. Understanding every little aspect and being able to prove every part is now a luxury, and interfacing without real understanding is the more practical approach in the turbulent waters of poorly connected technological and social systems.
But still there is hope and there are dreams. We like to drench ourselves in dream qualia sometimes, and Haskell and pure math are that medium. The abstractions of it, the consistency of it, the purity of it... When Haskell is called a pure language, it almost goes beyond the static definition of functions being pure, and describes the general feeling that occurs when writing Haskell. You feel pure. You feel like you are taking these small parts and creating greater parts in an elegant buildup of abstractions, traversing one level higher and one level lower at your whim.
Lisp... maybe it’s the parentheses, maybe it’s something else... it never really caught onto me like Haskell did. Haskell feels pure and dream-like and perhaps unsuited to the world where (if you really get down to it) abstractions and types are just useful ‘human’ inventions and unfit for every usage. The world is for getting down and dirty, and mathematics, or at least the pure side of it, really isn’t. The representative mathematics of Haskell is Category Theory, and it is just as far from the level of “real” as it can be. More abstract than abstract algebra, if you will.
Abstraction itself is an intellectual operation that is also rooted in emotional detachment. Perhaps Haskell represents that kind of ideal in a modern world where practicality pays before purity.
For me too there is a feeling of poetry... but for me it's more the nails-on-blackboard grating of awful poetry, and the ice-pick-to-ears screeching of abrading subway wheels. :)
Lisps merely inspire me towards elegant craftsmanship. But Haskell tempts me to dream of more. It's my fault, but ouch.
Dreams of math made manifest. Of materializing a lattice of theories, each with types and operators and laws, simply extensible and composable - a push-out lattice. Dreams of programming that's more math than plumbing. And maybe someday, some future Haskell Next will permit that flight, that dance. But that's not Haskell.
Someone I know, will pull up the deep math of a problem domain, model it in Coq, and mentally compile it to Haskell, extending GHC internals as needed to better serve as compiler target. For a wetware compiler that gets only very limited support from current tooling. I can't do that. Not even close. I need a language and tooling that's on the same page as me, to serve as an extended self. And for this, that's not Haskell.
Many years back, someone in a conversation suggested a core skill of a professional C++ programmer, was cataloging the things which seem like good ideas, and might even serve well in some other OO language, but which tended to go badly in that era's C++. Along with many baroque workaround to salvage others.
Fewer years back, I'd a conversation about using Haskell in education. We talked of alternate preludes to remove historical clutter, and extensions to remove historical limitations. And turned up a fun divergence - he thought of learning Haskell as fairly easy, and I thought of it as ghastly hard. For his default context was programming in the small - vocabulary, functional mindset and such. And mine was... you the start at the other end, with the dream, the no-extraneous-complexity pure essence of what is needed... and now you have to learn the zoo of baroque work arounds, and crippling limitations, by which Haskell fails the dream.
Even with category theory, the overwhelming emphasis is on using it for proof rather than for its expressiveness. A fun example... from a very collegial course in pre-Covid January, sniff... Categories work over PreSets - no predicate of member equality required. But say I claim to be handing you a PreSet. You can't verify that claim. Your ability to prove anything is very limited. Sad for mathematicians. But fine as API. But that's not where attention is.
That feeling you describe quickly goes away when you use the language everyday. Once that happens, you can start weighting it as a tool.
And overall, my assessment is that purity and practicality are not competing aspects. I would say that purity conttibutes to the practicality of haskell.
This is a matter of feeling, completely irrational, but just my experience.
When I program in lisp I get the same feeling when I'm solving an ODE by hand, or a Diophantine equation, or designing a numerical method to approximate the solution of a PDE, or finding the Euler-Lagrange equations of a physical problem. The thing is real and it gets shit done really fast; it is just exhilarating. Moreover, lisp macros are so dirty and fun that using them feels like kinky sex.
Haskell is like category theory. Sure, it is pure and general, and you can create set theory and the rest of math from it. But it is certainly the most boring thing that I can think about.
But hey, whatever floats your boat.
(I prefer algebra and Haskell. But I like Lisp well enough.)
> I followed a (pure) mathematics education...
I'm not so quick in judging people by how applied they are.
We mathematicians always try to come up with new pure fields, but then some other people always come and find some applications. Just see how cryptography and computer science sullied our beloved number theory!
(I mostly focused on the computer-science-y parts of math in my studies. Thinks like linear optimization and combinatorics. Not sure whether to count that as applied or pure. I guess I might get a purity pass, because we only ever proved our algorithms correct but seldom implemented them.)
If I want to solve a problem I can think of it a a series of maps f: X -> Y and once I solve it on paper "translating" into Haskell just feels natural.
It's also super easy to do that in clojure, maybe I'm just baised because I found and learned Haskell first.
Now days I use clojure for everything and it's amazing. I never enjoyed programming so much until I discovered the functional paradigm. It just feels right.
[1] https://hackage.haskell.org/package/tardis-0.4.1.0/docs/Cont...
take 5 . filter (not . p) . drop 3
becomes (->> s (drop 3) (filter (complement p)) (take 5)) ; for some sequence s
I think it is also true of Clojure that it strives to have many small functions with a high degree of composability.Now there's a different discussion to be had about laziness and immutability but for a lot of stuff it doesn't really come into play.
We have laziness in Clojure but don't think we have stream fusion. Yet the functional approach works just fine for most things and is preferred.
Even in JS which doesn't even have built-in laziness most new stuff uses a functional approach (underscore,lodash etc) wherever possible too.
CL is just unique in that it has a very long history and a huge standard library. They have many of these monolithic functions that other languages simply don't have built-in.
Edit: corrected “—>”.
myList
& map someFunction
& filter somePredicate
& sum
Other ML-ish languages typically provide this operator but under a different name, |> being a common one. One important note is that the mechanics here are a bit different than threading macros in lisp, since you're explicitly building the AST you want and using operator precedence (and implicit/automatic currying) to get the visual effect, rather than calling a rewrite rule. This means you can't have a straightforward reproduction of Clojure's -> macro, although in practice this never really matters, and when it does you can still fake it with flip (or, more readably, parens and a lambda).Clojure's threading macros arise because it's a little bit more awkward in Clojure to partially apply functions (either anonymous functions with # or use `partial`) and it reads more fluently to just use threading macros there.
However, Clojure can do fancier things with variants of the common threading macros (https://github.com/rplevy/swiss-arrows)
Function application always has higher precedence than infix operators so I can't emulate thread-first with functions in Haskell. If that wasn't the case, this would work (this was what I was thinking of)
(|*>) :: b -> (a -> b -> c) -> (a -> c)
(|*>) x f = (\y -> f y x)
xs = 5
|*> take [1, 2, 3]
Otherwise, I can come close with this. import Data.Function ((&))
infix 9 *-
(*-) :: (a -> b -> c) -> b -> (a -> c)
(*-) f x = (\y -> f y x)
xs :: [Int]
xs = 5
& take *- [1, 2, 3]
& filter (/= 1)
& head
& take *- [1, 2, 3] (*-) :: (a -> b -> c) -> b -> a -> c
(*-) f x y = f y x
This reveals that (*-) is simply flip, which is in the Prelude (re-exported from Data.Function). Alternatively, Hoogling the original type signature will reveal the same thing.So the example doesn't require any new functions:
import Data.Function ((&))
xs :: [Int]
xs =
5
& flip take [1, 2, 3]
& filter (/= 1)
& head
& flip take [1, 2, 3]
In general, it's common in Haskell to use flip, curry/uncurry, and other higher-level functions to manipulate functions so they fit into the needed context. xs :: [Int]
xs =
5
& (`take` [1, 2, 3])
& filter (/= 1)
& head
& (`take` [1, 2, 3])
But I'm not sure that's a better style than flip.I really like `on` from Data.Function, too.
xs =
5
& (\n -> take n [1, 2, 3])
& filter (/= 1)
& head
& (\n -> take n [1, 2, 3])It depends on what you are used to and your tolerance thresholds.
In a code base I worked on professionally, I came across the following idiom
f a b `flip` d
which at first used to confuse me to no end. Later I figured out that you are supposed to read it as: f a b <place-holder> d
and thus \c -> f a b c d
In the same vein that Scala uses an underscore in somewhat implicit anonymous function syntax.I still wasn't really happy with that usage, but at least I see why someone thought it would make sense.
(Mostly I wasn't so happy because it only works for the penultimate argument. If the syntax had worked out so that
f a `flip` c d
would mean \b -> f a b c d
I would have been more amenable to the idiom. But you need the ugly f a `flip` c `flip` d
for that case. And that's clearly worse than the lambda syntax. flip :: (a -> b -> c) -> b -> a -> c
[1]: https://hackage.haskell.org/package/base-4.14.0.0/docs/Prelu... Prelude> s ->> f = f s
Prelude> [1..100] ->> take 10 ->> filter (not . odd) ->> drop 3
[8,10]He's quite brilliant, but I still haven't really gotten used to the full power of lenses, yet. I only used them in simple ways.
I may be mistaken, but I think I have seen a handful of "Lens-lite" type libraries for use in production since it's such a large library, which seems.. uh.. suboptimal.
You can smash a list of functions together but they'd all have to take and return the same type, and the list would introduce commas and square brackets, so it wouldn't look quite the same.
It feels so much easier to both understand and modify the evolved version of the code.
This one made me laugh out loud: "This is sometimes called parametric polymorphism. It’s also called having your cake and eating it too."
And I'll definitely read more of that blog.
Heavy use of Composition is idiomatic in lisps, too. It’s just the syntax doesn’t rub your face in it: It’s pretty common to see form in lisp that ends in a dozen close-parentheses as we see chains of forms being passed as arguments to other forms. It would be weird if composition wasn’t idiomatic in a language with first-class functions.
The loop macro isn’t good evidence: it’s notable precisely because of its difference from typical lisp usage. I’ve heard lisp programmers claim to refuse to use loop because it clashes with the rest of the language. I’m less interested in purity, and it is so convenient sometimes...if only as a clear illustration of just how much power and potential for abuse the CL macro system provides. Amd let’s just leave CLOS and metaobject protocols under the tarp for now :-)
That's just a chain of function applications.
Function composition puts the emphasis on the fact that functions are values in their own right and can be manipulated (eg with a composition operator) as objects without regard to any values they might act on.
If you want to see one Haskell equivalent of loop, https://hackage.haskell.org/package/monad-loops-0.4.3/docs/C... has got you covered.
John Backus seemed to think it was a big deal. See eg 'Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs ' (http://www.ict.nsc.ru/xmlui/bitstream/handle/ICT/1256/1977-b...)
On the one hand, I do agree that in a language like Haskell it's almost entirely a syntax level distinction.
On the other hand, syntax level conveniences and inconveniences can have a big impact on how people use a language. (Eg in Haskell if-then-else is more hassle to type out than pattern matching. A slight nudge by the language design to make users prefer the latter.)
Once you have acquired the ability to see functions as objects in their own right, many more techniques become available. Think of eg parser combinators.
Technically you don't need the syntactic convenience to use parser combinators. But I am not sure they are worth the hassle without those conveniences. Eg Python has enough functional machinery to support parser combinators, but its options for defining functions (def and lambda) are just so cumbersome.
(Slight pedantry: I wouldn't call it the 'loop monad' in the same way as 'list monad' or 'IO monad'. The monad loop library provides looping combinators you can use with many monads and some you can use with all monads.)
If we talk about the approach of using larger building blocks with named parameters, that's also typical in UNIX, since most UNIX commands have zillions of named arguments, even more so than typical Common Lisp functions. Typically it is easier in using interactive command languages to fill out common options to a command, than to write programs.
For example 'ls' does not expose a bunch of functions to combine, but is a powerful command with lots of named options:
https://man7.org/linux/man-pages/man1/ls.1.html
Named parameters/arguments are not generally common in Lisp. Emacs Lisp did not have them and wanted to avoid them explicitly.
[1] https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node347.html
https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
I also think it’s a bit unfair to say that Haskell’s stream fusion requires special compiler support. The compiler doesn’t recognise list functions specifically or have special cases for list-looking-types (except for syntax). Instead there is a general mechanism to say “at stage number x, replace expressions that look like y with z”, where there are something like 10 stages and expressions y and z are type checked so these transformations are only applied in “valid” cases. I think saying that this is special compiler support for stream fusion is like saying that macros are special compiler support for series in CL.
A final thing to note is that because of lazy evaluation, stream fusion in Haskell doesn’t change the semantics of the Haskell list functions from how they would behave without it. In Common Lisp streams must be explicitly separated from lists because they are semantically different (in the evaluation order of the functions that act on them).
[1] https://markkarpov.com/tutorial/ghc-optimization-and-fusion....
It's an optimizer for a graph-reduction engine.
It doesn't mean much to call out "special support" for any one bit of style of programming, since the language itself has nearly no concept of performance -- you just write functional code and the compiler will find the fastest runtime computation of the evaluation that it can.
While GHC is glorious, it's still very limited in what it can do, performance wise.
You are right that languages themselves seldom have a direct concept of performance. But language features and restrictions have a big impact on achievable performance. Eg Haskell's purity-by-default means that the compiler has quite a bit of freedom when choosing how to translate something like eg the 'map' or 'filter' functions.
The equivalent in C++ gives the compiler less flexibility, because the helper function handed over at runtime might have side-effects.
Similarly, Haskell functions are still allowed one important side effect: non-determination. In a more restricted language like Agda that's not allowed. Thus giving the compiler more degrees of freedom to work with.
Or in a different direction: the query optimizers for SQL can do very impressive work, because SQL is so limited.
Yes. Though if we were starting from scratch today, perhaps we would see a total language, ie one where all functions terminate.
That would allow the compiler much more freedom in how to order evaluation.
Lisp did in fact pioneer the introduction of higher-order functions and function composition into programming language and its fairly remarkable that lisps are still around and highly versatile / usable.
What's being shown are Collection Pipelines(https://www.martinfowler.com/articles/collection-pipeline/) which can be implemented in lisp and in fact are. Clojure and Scheme for sure have these procedures and I am sure they are available for common lisp as well.
Whether one prefers Haskell or Lisp is probably in parts a subjective decision (and also has to do with socialisation). I think I am falling onto the lisp side of things because of the explorative nature of programming and I had my heureka moment of programming with Test-Driven Development and for some reason I am gravitating to that in my programming and its easier to do TDD without a type-checker actually.
For anyone who is curious about exploring Lisps, I suggest having a look at Clojure or Common Lisp (potentially Dylan if you don't want to bother with parentheses) and not Scheme (because it doesn't have polymorphism and is heavily fragmented).
[1]> (remove-if-not #'evenp `(1 2 3 4 5 6 7 8 9 10) :count 3 :start 1)
(1 2 4 6 8 9 10)
As pointed out by owl57, the Haskell translation is incorrect and the correct implementation isn't quite so trivial. I don't see a way to pull the :count argument out into a separate function. We certainly can for skip, though, and I might. Prelude> skipThen 1 (removeIfNot even 3) [2,3,4,5,6,7,8,9]
[2,4,6,8,9]
as implemented: removeIfNot p count = go count
where
go _ [] = []
go 0 list = list
go n (x:xs) | p x = x:go n xs
| otherwise = go (n - 1) xs
skipThen count f list =
let (pre,suf) = split count list
in pre ++ f suf import Data.Bifunctor (bimap, first)
type Split a = ([a], [a])
unsplit :: Split a -> [a]
unsplit = uncurry (++)
splitStart :: [a] -> Split a
splitStart xs = ([], xs)
skip :: Int -> Split a -> Split a
skip n (xs, ys) = first (xs ++) (splitAt n ys)
iterateN :: Int -> (a -> a) -> a -> a
iterateN n f = (!! n) . iterate f
removeOneIfNot :: (a -> Bool) -> Split a -> Split a
removeOneIfNot p (xs, ys) = bimap (xs ++) (drop 1) (span p ys)
GHCI> unsplit . iterateN 3 (removeOneIfNot even) . skip 5 . splitStart $ [1..15]
[1,2,3,4,5,6,8,10,12,13,14,15]Still, definitely a more complicated decomposition than that described in the article (but maybe a more interesting article!)
stepSplit :: ([a] -> Split a) -> Split a -> Split a
stepSplit f = uncurry first . bimap (++) f
skip n = stepSplit (splitAt n)
removeOneIfNot p = stepSplit (second (drop 1) . span p)
Or in perhaps-more-familiar monadic terms: import Control.Monad (replicateM_)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.State (StateT, evalStateT, state, get)
import Control.Monad.Trans.Writer (WriterT, execWriterT, tell)
import Data.Bifunctor (second)
import Data.Functor.Identity (Identity, runIdentity)
type SplitterT b m = WriterT [b] (StateT [b] m)
type Splitter b = SplitterT b Identity
splitter :: Monad m => ([b] -> ([b], [b])) -> SplitterT b m ()
splitter f = lift (state f) >>= tell
execSplitterT :: Monad m => SplitterT b m a -> [b] -> m [b]
execSplitterT m = evalStateT (execWriterT (m >> lift get >>= tell))
execSplitter :: Splitter b a -> [b] -> [b]
execSplitter m = runIdentity . execSplitterT m
skip :: Monad m => Int -> SplitterT b m ()
skip n = splitter (splitAt n)
removeOneIfNot :: Monad m => (b -> Bool) -> SplitterT b m ()
removeOneIfNot p = splitter (second (drop 1) . span p)
GHCI> execSplitter (skip 5 >> replicateM_ 3 (removeOneIfNot even)) [1..15]
[1,2,3,4,5,6,8,10,12,13,14,15]
It's hard to say which version is clearer. It probably depends on what you're used to. The code is essentially the same under the Writer/State abstraction, with `execSplitter` replacing both `unsplit` and `splitStart` and monadic bind in place of function composition. The `Splitter b ()` type is isomorphic to the `[b] -> ([b], [b])` used for the argument to `stepSplit` in the first version. (1> (let ((r (range 1 10)))
(reject r (take 3 (drop 1 [where oddp r]))))
(1 2 4 6 8 9 10)
This is not correct because "drop 1" means "drop the first index where r was found odd" not "drop the first index if it happens to be zero".Fix:
2> (let ((r (range 1 10)))
(reject r (take 3 [drop-while zerop [where oddp r]])))
(1 2 4 6 8 9 10)
"Let R be a sequence of integers, indexed from 0. Determine the indices where R is odd, as a sequence of indices in ascending order. Remove the zero index, if it is present. Then take the first three of the remaining indices. Remove the elements with those indices from R."This seems verbose, but watch it do something Common Lisp's remove-if will choke on:
2> (take 50 (let ((r (range 1 1000000000000)))
(reject r (take 3 [drop-while zerop [where oddp r]]))))
(1 2 4 6 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
48 49 50 51 52 53)
range generates a lazy list. where generates the indices lazily. drop-while and take are also lazy, and I just coded reject so that, after the indices have been exhausted, if the remainder of the list is lazy, then it just tacks that lazy suffix into the output. I.e. it's not fully lazy, but in this case it works because the list of indices was trimmed to a finite length.I think in release 242, I will might include reject, as a fully lazy version supporting a lazy, infinite list of indices as well as sequence; and will fix select to also be fully lazy.
> UPDATE 2020-08-03: I no longer stand by the content in this post. I think the overall sentiment is marginally accurate; however, the details in the post are incorrect (as many have pointed out over the years).
> As has been pointed out, remove-if-not’s start/count parameters behave differently and cannot easily be separated out of the function, a design trade-off that I appreciate.
The start argument specified where to start removing things and the end or count arguments specify where to stop removing things (ie at an index or after a certain number of elements respectively).
I claim this is a pretty good argument against the keyword arguments as they don’t necessarily do what one expects.
When I see code saying (remove-if #'condition '(1 2 3 4 5 6) :start 5 :count 3), I would be pretty surprised if it removed anything prior to the 5th element. :count is worse, just by looking at this code I would not be sure whether it would always remove 3 elements (at most) or if it would remove all elements within a subsequence of 3 elements starting at index 5.
Monoliths tend to be better at solving common real life problems efficiently while composition is better suited for unexpected abstract problems. We need both.
For example GNU binutils are not very "unixy", with plenty of "monolithic" options but they still work well with pipelines.
Meanwhile in lisp I would have written a special drop3-take5 function in the first place. Then refactoring it would just take replacing 3 with n and 5 with m, adding n and m as arguments and writing a function to read whatever the config file is.
foo (n, m) = take n . filter p . drop m
bar = fmap (fmap foo) getNM
It's a little awkward, but no major refactor. A small price to pay for the nice types we get. I'd argue that if other languages make dealing with IO and optional values easier, its because they don't bother restricting IO (not necessarily criticizing that), or they don't bother checking completeness over optionals (i.e. a stray `null` can show up anywhere and derail the program, which I do think is a bad thing). Overall, Haskell provides many powerful tools for composing complex types, such as the functor/applicative/monad classes.Pattern matching on Maybe types is rather unidiomatic Haskell. Much better to take advantage of the type classes to which Maybe belongs: Functor, Applicative, Monad. In particular, there's no need to unwrap/wrap the Maybe value when you have `fmap`.
dropNTakeM p n m = take m . filter p . drop nThe "Common Lisp" version:
(for [i (range 1 10)
:when (even? i)
:while (< i 5)]
i)
A Haskell-y version (according to the article): (->> (range 1 10)
(take-while #(< % 5))
(filter even?))
Using Clojure's transducers: (into [] (comp
(take-while #(< % 5))
(filter even?))
(range 1 10))Common Lisp promotes the use of doclines and apropos tools in order to discover and understand behaviour, with symbol names that tend to describe the function of what they represent.
Haskell leans on type signatures and symbolic operators.
IMHO, a pseudo-random sequence of non-word symbols tied to a type signature is of very little use.
IE, something fabricated:
(-*/) :: A -> M c -> B
Ah yes, so understandable.On the other hand, it's also the case that too any Haskell projects are under-documented. Probably worse than the typical language, and certainly worse than the best-in-class.
On the gripping hand... while types make poor documentation, they are always correct documentation, and they are automatically generated documentation. When I write Python or (apparently) Common Lisp, I can expect to get reasonable documentation by interrogating anything someone hands me. In Haskell, insofar as I have learned to get good information from the types, I get some (correct!) documentation for things that I have newly assembled myself!
* (describe #'+)
#<FUNCTION +>
[compiled function]
Lambda-list: (&REST SB-KERNEL::NUMBERS)
Declared type: (FUNCTION (&REST NUMBER) (VALUES NUMBER &OPTIONAL))
Derived type: (FUNCTION (&REST T) (VALUES NUMBER &OPTIONAL))
Documentation:
Return the sum of its arguments. With no args, returns 0.
Known attributes: foldable, flushable, unsafely-flushable, movable, commutative
Source file: SYS:SRC;CODE;NUMBERS.LISP
Docstrings and type information can (optionally, but recommended) be added to all code. Users of your API can use documentation/describe to discover information about it without ever having to look at the implementation.Having only ever dabbled in Haskell, I feel the same way, but I'm aware that proficient Haskell programmers don't tend to agree. How do you rate your Haskell skills?
I've heard the same said of another 'weird and wonderful' language: Forth. Yes, it's incomprehensible if you aren't familiar with the language, but that's not really who the language is for.
I'm surprised no one has plugged Hoogle. It was the hands-down best resource to make Haskell reasonable for me. But even still, I found code that I wrote in Haskell not unlike the plentiful amount of code I wrote in Perl: returning to it after the fact was tedious.
let's try to assign some meaning to this. From the get go, you know that it's a function that turns an A into a B, and it only has the information that is derivable from `M c`.
So, yes this is gibberish. But if there's a slight modification to this:
(-*/) :: A -> M c -> c
then it makes a lot of sense. Signatures are a much shorter, terser way to communicate than words (and nesting structures that LISP uses) imho.But even with your alteration, I'm not sure what it does. What interaction is there between A and M c? Does it mutate c, or just unwrap it?
data M c = M (A -> c)
there would only be one reasonable implementation: a -*/ (M f) = f a
If `M c` does not contain a function from `A` to `c` then the only possible result is non-termination. You weren't given a value of type `c`, and you have no way of producing one from the inputs, so you can't return such a value, and the only alternative is not returning at all.Of course the real problem here is that (at least without context) this operator name doesn't mean much. You could make the same complaint about a perfectly ordinary function named `ixfni`. The only real difference in Haskell is that symbolic operators is written infix by default, while named functions are written prefix, but it's very easy to flip that around:
(-*/) a b
a `ixfni` b
Note that you can even define fixity and precedence for named functions in their infix forms. Unlike most other languages with user-defined operators there is remarkably little pressure to rely on symbols in situations where they would be unclear. Symbolic operators should only be defined in situations where the symbols are reasonably well-known (within some context) or "intuitively" related to existing operators.This is because Haskell’s lazy evaluation allows these compositions to do an asymptotically appropriate amount of work. Consider this Haskell:
xs = [1..20]
p n = if n < 13 then even n else p n
ys = take 5 . filter p . drop 3 $ xs
main = print ys
If a strict language were being used then the processing would be as follows:1. drop 3 ys is [4..20] 2. When computing filter p [4..20], everything is ok up to 12, but computing p 13 goes to an infinite loop. 3. We never get a result to compute take 5 of.
Because Haskell is lazy, it will not try to compute p 13 and so can do the right amount of work.
Perhaps a more reasonable example would be that if the list xs were really long, a strict program would have to evaluate p against every element before taking the first 5 results (so it would take linear time if p were constant time) while a lazy program would only need to compute p on enough elements to collect 5 results.
Common Lisp was designed for older, slower computers than Haskell and to be able to make programs which were reasonably efficient. It also had strict evaluation and so with this context, the extra arguments to many functions for special cases (which lots of people didn’t like) or the combined iteration of loop (which lots of people didn’t like) make a lot more sense.
If you have laziness by opt-in, then most parts of the language will not opt-in [from prior experience with, say, racket which also has lazy streams].
It's the pervasive and efficient compilation of laziness in Haskell which makes it actually useful to write lazy programs.
Now, am I a fan of writing lazy programs? No, I find the upshot of referential transparency gained by laziness to be overshadowed by the warts it brings --- lack of a good debugging experience, tie-the-knot semantics being fragile, etc. I'd rather live in a strict total programming language.
But one cannot "handwave" away laziness in haskell that easily by saying "you can emulate it in $STRICT_LANGUAGE". By the exact symmetric argument, one can emulate strict code inside haskell: just use `ST` or `IO`. But very rarely does one see whole libraries written strictly, because this style of code doesn't compose with the (default) laziness and purity in haskell.
https://wiki.call-cc.org/man/5/Module%20(chicken%20string)#s...
Because Chicken's `string-split` expects its delimiter to be optional, it wants a string first.
https://hackage.haskell.org/package/text-1.2.4.0/docs/Data-T...
While Haskell's emphasis on composition means it's more logical for the constant parameter—the delimiter—to be provided first.
It's not a great example, but the first one I can think of. The difference is starker when the Lisp command has many more parameters than the Haskell one, with some of them being optional. When trying to write cute point-free code in Scheme, I found myself using `flip` too much for it to be fruitful.
This might seem like huge overkill, but when the specs change and I need to set the start and stop from a config file I can easily refactor drop3-take5 into (drop-n-take-m n m test list), with a let around it to read the config.
drop3-take5 would look something like:
(define drop3-take5-list
(lambda (ls filter)
(define internal
(lambda (ls matches)
(cond
((null? ls) '())
(else
(if (filter (car ls))
(cond
((< matches 3) (internal (cdr ls) (1+ matches)))
((<= matches 5) (cons (car ls) (internal (cdr ls) (1+ matches))))
(else '()))
(internal (cdr ls) matches))))))
(internal ls 0)))This is a bad example because it is so contrived to show off the strengths of haskell.
As an example for the strengths of lisp: I wrote a logical dsl that adds a boolean dependent type system to scheme using macros. It's 400 lines long and copies all the bits from Idris I needed and it still feels like a scheme and is fully interoperable with regular scheme.
I can trace it well less well in guile which is the usual scheme I use for daily projects but it's not something I've had huge trouble with on medium sized projects >10,000 lines.
* Lisp is multi-paradigm languages. Haskell is pure functional language.
* Haskell wants to be mathematics. Lisp wants to be an operating system.
I believe the composition vs. monolithism difference comes from these deeper differences.
Lisp wants to be interactively used.
[x for x in foo if x not in y]
Turns out, this can be expressed in Lisp: (list x for x in foo if x not in y)
I've implemented this into my fork of Lumen that runs on Python. https://github.com/shawwn/pymen It feels very nice to use: > (list x for x in (range 10) if (= (% x 2) 0))
"""
Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list.
The argument must be an iterable if specified.
"""
[0, 2, 4, 6, 8]
>
It wasn't even that ugly to implement. It's ~20 lines: https://github.com/shawwn/pymen/blob/1c191c9e00e73303a6479f8...One other neat thing is that the REPL prints out the docstring of the last evaluated thing by default. It makes it nice to explore libraries:
> (import numpy as np)
https://imgur.com/1rpF95y > np.vsplit
https://imgur.com/OjIvTWtIt's like an automatic help() call on the last evaluated thing.
(If anyone happens to try it out, you can get into a REPL by running `rlwrap bin/pymen`, or just `bin/pymen`.)
But list comprehensions suffer from the same problem the author is taking about, they don't compose. Its even worse than CL remove-if variant, where one can reify the filter into a function that can then compose.
[x| x <- foo, not (elem x y)]
Here's an example finding consonants: [c| x <- ['a'..'z'], not (elem x) $ "aeiou"]A nice thing about Haskell list comprehensions is they're based on a trivial transformation to monad syntax, which makes it easier to factor out complex list transformations into multiple little bites. I've run into this problem in Python. Also, Python list comprehensions can often behave very unexpectedly due to mutability.
Here's an example of the monad syntax:
[(x,y) | x <- [1..10], y <- [1..x], odd (x + y)]
equiv do
x <- [1..10]
y <- [1..x]
guard $ odd (x + y)
return (x,y)
Lots of combinatorics problems can be elegantly solved using the list monad like that. CL-USER> (let ((foo '(1 2 3)) (y '(2 4)))
(loop for x in foo unless (member x y :test #'=) collect x))
;; -> (1 3)
It wouldn't be hard to write a macro that expands your expressions to CL's loop, with a bit of magic to parse the "if ... [not] in ...".> import whatever > filter(_ not in y, foo)
For me it's more readable. To each their own, of course.
Many of the language extensions are sugar over a simpler (implied) core of the language. Some are purely syntactic, like NumericUnderscores. Some others like GADTs are semantic, but still mostly explained in terms of mapping to this simpler language.
Have you ever looked at a Unix man page?
And while Common Lisp has some functions that might be swiss army knives of functionality, that is not "the philosophy of Lisp", it's a particular style. Clojure and Scheme are just as much Lisp as CL, and as other people have noted, their style is very similar to Haskell.
Chris and I probably differ in that I love the kitchen sink built in functions. Also, it is also common to write lots of very short Common Lisp functions.
> Like pipes in UNIX, the functions are clever enough to be performant when composed together–we don’t traverse the whole list and generate a new list each time, each item is generated on demand. In fact, due to stream fusion, the code will be compiled into one fast loop.
I imagine it is hard to do the same optimization in a non-lazy language.
How is that order of operations different to stream fusion? Why does stream fusion require more purity than Unix pipes?
You can do the same optimization Unix pipes does without language enforced purity, just with a caveat that functions sharing state might behave weirdly when fused.
[1] http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...
This honestly seems like a misunderstanding which ignores how many POSIX commands actually accept commands. The uni philosophy doesn’t mean literally one thing, but rather one task; sometimes the task will have different parameters and that’s OK, but we’re not going to have `ls` takeout the trash.
I would rather say that each tiny function handles one factor and that a program is the decomposition into factors. Lisp also composes functions but are chunkier and can be ad-hoc one-offs. Static types vs runtime errors is another point of difference.
Javascript:
a = [...Array(20).keys()]
p = x=>!(x%2);
a.slice(3).filter(p).slice(0,3)
Python: a = range(20)
p = lambda x: not x%2
filter(p, a[3:])[:3] (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15) :count 5 :start 3)
(1 2 3 4 6 8 10 12 14 15)
a = (1..15)
r = []
c = []
a.each.with_index do |e, i|
if i <= 3
r << e
elsif c.length < 5
if e.even?
r << e
else
c << e
end
else
r << e
end
end
r
#=> [1, 2, 3, 4, 6, 8, 10, 12, 14, 15]
I can see no simple way to compose this a = (1..15)
l = a.take(3)
c = 0
m = a.drop(3).map.with_index { |e, i| [e, i, e.even?] }.take_while { |e, i, p| c +=1 unless p; c <= 5 }
_, last_m_index, _ = m.last
m = m.filter { |e, i, p| p }.map { |e, i, p| e }
r = a.drop(3 + last_m_index + 1)
l + m + r
#=> [1, 2, 3, 4, 6, 8, 10, 12, 14, 15]but the slices here would spoil it
there is https://docs.python.org/3/library/itertools.html#itertools.i... islice however...
While I prefer Haskell, judging Lisps by Emacs Lisp isn't entirely fair. It's probably the worst Lisp in somewhat wide use.
('newLisp' is worse, of course. But thankfully no one in their right mind uses it.)
The remove-if-not function just a Common Lisp thing.
And, by the way, at the bottom of its description in the standard, we find this:
The functions delete-if-not and remove-if-not are deprecated.
Prelude> take 5 . filter even . drop 3 $ [1, 2, 3, 4, 5, 6, 7, 8, 9]
[4,6,8]
(remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9) :count 5 :start 3)
(1 2 3 4 6 8)Isn't composition literally the benefit of S-expressions, prefix notation, and everything being a function?
And functions aren't that important in CL, not everything is a function and CL is usually written in an OOP or procedural style. CL is more about data and AST composition then anything else.
Moreover, CL is a lisp-2, so in a very real sense, functions are second class citizens in the language.
https://github.com/eschulte/curry-compose-reader-macros
Functions are not second class citizens. They are privileged with their own namespace. That's a perk, not a penalty.
One notable form of composition in CL is method combination. Does any other language support something like that?
I completely disagree that it sucks. What sucks is having to remember not to have your variable names collide with your function names (or indeed any of the other namespaces in CL). Things that are usually used in different ways benefit from having separate namespaces.
Sure composability is nice and should probably be the way you start creating something but after you are satisfied that it works the way you want it to and you want to make it more efficient you will go monolith and treat this new thing as a bigger piece you can compose with.
Someone new can the come along, fail to understand why its a monolith and decide to make it better my making it made up of composable parts again.