Lisp and Haskell (2015)
markkarpov.com
markkarpov.com
This is SBCL 1.5.6, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
* (defun add-text-padding (str &key padding newline)
"Add padding to text STR. Every line except for the first one, will be
prefixed with PADDING spaces. If NEWLINE is non-NIL, newline character will
be prepended to the text making it start on the next line with padding
applied to every single line."
(let ((str (if newline
(concatenate 'string (string #\Newline) str)
str)))
(with-output-to-string (s)
(map 'string
(lambda (x)
(princ x s)
(when (char= x #\Newline)
(dotimes (i padding)
(princ #\Space s))))
str))))
; in: DEFUN ADD-TEXT-PADDING
; (MAP 'STRING
; (LAMBDA (X)
; (PRINC X S)
; (WHEN (CHAR= X #\Newline) (DOTIMES (I PADDING) (PRINC #\ S))))
; STR)
;
; caught WARNING:
; The function (LAMBDA (X) :IN ADD-TEXT-PADDING) called by MAP returns NULL but CHARACTER is expected
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
ADD-TEXT-PADDINGThe Common Lisp implementations and compilers keep on getting better with time and they produce more and more compile-time warnings (especially SBCL).
Now, some discourage the use of type declarations in Common Lisp (for a variety of reasons, one being the limited portability as less clever compilers don't warn against mismatch, but might produce broken code) and suggest the (run-time type testing) CHECK-TYPE (standard) macro instead. SBCL's type inference (at least in recent versions) is sufficiently clever to detect mismatches at compile time, when the type is (implicitly) announced using CHECK-TYPE.
Which case I clearly see here.
Common Lisp isn't my usual dialect but I'd've expected something more like:
(defun add-text-padding (str padding)
(let ((lines (split-string "\n" str)) (
cond
((nil? lines) "")
(#t (join-string "\n"
(cons
(car lines)
(mapcar
(lambda (x)
(concat-string
(repeat-string " " padding) x))
(cdr lines)
)
)
))
)
)
Note: pseudocode typed straight into the comment box, and I dropped the newline feature, but hopefully a bit more illustrative of what I'd consider 'normal' lisp.(comments, criticisms and thrown fruit welcome)
I tend to do single assignment and minimal mutations in any language though, and try and push my side effects (I'm aware the string write is encapsulated therefore not a side effect) as far out to the edge as possible.
As an example, in perl (which is probably my most-used language, so I'm 100% on board with "more than one way to do it" ;) I'd've written:
sub add_text_padding ($str, $padlevel) {
my ($first, @rest) = split /\n/, $str;
my $pad = ' ' x $padlevel;
join "\n", $first, map $pad.$_, @rest;
}
(my lisping has indeed been significantly in scheme, plus dabbling in other dialects and a tendency to prototype things in operative lisps derived from Shutt's kernel ... which is why I did, indeed, ask for it, so thank you for the reply) (defun add-text-padding (str padding)
(let ((lines (split-string "\n" str)) (
cond
((nil? lines) "")
(#t (join-string "\n"
(cons
(car lines)
(mapcar
(lambda (x)
(concat-string
(repeat-string " " padding) x))
(cdr lines)))))))
The closing brackets are supposed to be in one line, that much I know.I was trying to elucidate a different structure.
I'm always a little frustrated whenever I see this aphorism perpetuated since I think it gives the impression that a static type system is doing more than it is actually doing. In type systems that are used outside of academia, the main thing your static types are doing is checking whether the shapes of your data and functions all line up. With some small exceptions, that's it: the compiler will tell you if your LEGOs fit together, but not if you've built a knife instead of a fork.
I know what this aphorism is getting at, which is that in some domains and circumstances a lot of your bugs are "shape" errors, and it's gratifying when you're writing OCaml or Java and you can, say, refactor without hesitation, knowing the compiler will find even the most unused paths where you need to modify an invocation of your function. But it's been my and others' experience that there are many times where this isn't your most pressing concern, and could in fact be a pretty boring concern that you don't even want to pay the type annotation tax for. (I also think there's something to the idea that a static type system can instill too much confidence and encourage the pursuit of bad abstractions like 18-arg functions that can no longer be effectively reasoned about by an unaided human mind, but that's another topic.)
head :: [a] -> a
This is the type of a function "head" that takes a list of values of some arbitrary type a, and returns an a.if you feed this function an empty list, what will it do? You can reason from the type signature that the only thing it possibly can do is crash, since there's no way to produce a value of type a without knowing what a is in advance.
head (h:_rest) = h
head [] = error "Our function is badly behaved!"
To ensure that our function never crashes, we can encode the fact that our list is nonempty in a datatype. data Nonempty a = Nonempty a [a]
Ie, a nonempty list must contain a value of type a, and a (possible empty) list of as. Then instead of writing a function that crashes when it receives a nonempty list, we can write a function on nonempty lists that never crashes head :: Nonempty a -> a
head (Nonempty x _rest) = x
What I'm getting at is that, yes, this is just the "shape" of our data. But you can encode more interesting and valuable properties in that shape than might be apparent at first. There are many other interesting examples of using types to ensure that properties that we want to hold for our values, do in fact hold.Edit: I should probably add that "If it compiles it works" is absolutely not always true. It's more of a community in-joke than an actual belief. But it does turn out to be true surprisingly frequently.
head :: [a] -> a
in Prelude was a mistake, and the default should be to return a Maybe/Optional. Beginners are generally advised to avoid head, and all other partial functions in the Prelude, such as tail and the indexing operator, wherever possible, and rely on pattern matching instead, which is generally sufficient and more idiomatic for the relevant use cases. I agree that having the option to use a partial function is useful, escape hatches are important, but it shouldn't be the go to.Nonempty was just a simple example. You're right that it's not very commonly used (probably because it's not in Prelude), but the general culture of writing type safe interfaces and pushing guarantees to compile time %100 is a thing. A commonly used, more complicated example might be Servant, which is a web library which statically ensures your implementation meets an API spec.
Yes, I am aware that the Haskell community generally tries to push more guarantees to compile time, and that Haskell's ease of defining and using new types equivalent to existing ones but that signify some property helps a lot in this.
Still, there is some limit to this. For example, you have many libraries adding measurement units for Haskell, and many libraries doing linear algebra. But you won't find too many people doing linear algebra with measurement units, because the types of intermediate results explode too much (e.g. a type-safe unit-of-measurement matrix multiplication of 3x3 matrices has 18 type parameters, with complicated restrictions between them).
Most take Foldable or Traversable.
When it's relevant your functions can be specialized into Nonempty.
If the code is sound, it should have the same complexity anyways. I.e. putting a non-empty requirement in the function signature is just as complex as adequately handling an empty array in the code.
My personal struggle is that statically typed languages are annoying to prototype (I don't care if it segfaults on my laptop, I want to validate my idea). My struggle is prototyping in something that I can easily transition to a strictly typed version. I'm curious to try prototyping in Javascript and adding types when I move to production.
However I've tried something similar with Haskell. I think I prefer Haskell for prototyping these days. It turns out you can get a long way playing with the types. Often I spend more time prototyping the types and they guide me to the implementation which is often quite small.
It takes some training and work to get there but I think Haskell, for me, is a better prototyping language -- I don't have to deal with undefined, accidental prototype overloading, etc.
That particular 'writing against the compiler' approach lended itself to write-once, over complex type juggling code. Because why make it readable - if it compiles, it works, and if it works, there won't be any need to read it, right? Well, except you still get bugs, and then debugging anything is like making sense of brainfuck. Maybe smarter people than me can juggle a handful of types and operators simultaneously in their head - I can't.
My favourite example of 'just because it compiles it doesn't mean it works is' Maybe [a]. I've had a colleague scratch their head for hours over something like this:
userCount :: Foo -> Int
userCount f = length $ getUsers f
This compiled, but always returned 1. Why? Well, because turns out `getUsers` returned `Maybe [User]`, and not `[User]` as the programmer expected, and somewhat asserted in their mind with types. And it also turns out that in Haskell, `length` takes a `Foldable`, and `Maybe` is a `Foldable`. Thus, a `length` of a Maybe will gladly return 0 or 1, which makes perfect sense from the point of view of the type system, but zero sense when it comes to humans. Whoops. def getUsers(f):
return (logged_in_users(f), logged_out_users(f))
def userCount(f):
return len(getUsers(f))"Python provides many tools that can be used to solve programming woes. One such tool is its strong and readable syntax." Do you see how empty this sort of platitude is?
Similarly, one might say that a strong type system is a useful tool. That doesn't make it a panacea (and I don't think that this was the claim), but that doesn't mean one can't appreciate it as a good tool.
def get_users(f) -> Tuple[List, List]:
return (logged_in_users(f), logged_out_users(f))
def user_count(f) -> int:
return len(get_users())
Now it's clear this is just wrong code. To fix it: def get_users(f) -> List:
return logged_in_users(f) + logged_out_users(f)
def user_count(f) -> int:
return len(get_users())Also, you should annotate the type of your lists :)
def get_users(f) -> Tuple[List[User], List[User]]:
return (logged_in_users(f), logged_out_users(f))
def user_count(f) -> int:
logged_in_users, logged_out_users = get_users()
return len(logged_in_users) + len(logged_out_users)But, if that really is the reason for Maybe [a] over [a], I'd still prefer Either SomeUsefulErrorType [a].
My specification doesn't make a distinction between absent users and possible error responses because I assume that errors will be processed elsewhere and the getUsers function is applied as part of the "safe core" interface that is not concerned about error handling. If the specification was more precise (even more precise than in your suggestion, for instance, getUsers :: SafeIterable a => Foo -> a and length :: SafeIterable a => a -> Integer ), it would work differently.
The issue in the example has to do with the clarity of the intent encoded in the type signature, not the inherent issue of the type system that cannot guarantee safe runtime of the example. That's why I mentioned type constraints for specific iterables as a possible solution to the encoding problem.
A better example of the compiled-but-not-working case would be something that currently cannot be expressed well by the type system, something like async exceptions or access to closed resources (will be covered by linear types soon).
Haskell will never stop you from representing every value as String, and just throwing exceptions if the strings do not convert back, for example.
I know, not what you meant. But a custom sum type would not only allow you to return multiple different failure cases, you also wouldn't give it a `Foldable` instance with surprising semantics.
Plain old data types are seriously underrated these days.
Don't get me wrong though, I spent the first 10 years of programming with C and Java so I understand the comfort that a statically typed language provides. I just haven't found it to be a big enough issue over the past 7-8 years of programming in Python to really complain about it.
Frankly, if you were to ask me what the top 3 sources of my headaches in Python are, number 1, 2, and 3 would be, receiving a None value when I expected something else; but that's an issue with most languages.
Curious example, because that's absolutely something a sufficiently advanced and well-thought-out static type system can completely solve.
You're right that the type systems of C and Java do not meet this bar, however.
95% of annotations in the average haskell codebase are unneeded for compilation. People still write them, because they are a useful source of information when reading the code.
So tell me, if you feel like you don't want to write down that one line of comment which happens to be checked against the compiler, I guess you probably don't write tests or documentation either?
Why would any experienced programmer make such a blanket statement? As always, it depends.
Sometimes there is value in documentation (or whatever unchecked types in fancy languages), especially for stuff that is read a lot more, by other people, or yourself in the future.
For a lot of stuff there is no point in writing documentation. For example, if the code is obvious and pedestrian. Or if you'll just throw away everything, or refactor a month later.
And very often, writing documentation is not only a waste of time, but downright harmful, because code and documentation easily diverge.
My personal advice (not just for documentation) is to strive on the side of doing as little as you can get away with, and only introduce structure and bureaucracy if there's a clear indication that it's required.
Case in point: type aliases instead of (Haskell) newtypes. Type aliases (which are unchecked, unlike newtypes) can lead to exactly the kind of diverging that I alluded to. Look at the mess that is all the typedefs and defines in the Win32 API for example. It's extremely hard not to pass in the wrong typedefs, and there must be extremely few programmers who know which aliases are the same on every platform and which could change.
Aside, newtypes (Or the equivalent in C for example, wrapping a single member in a struct) can be very annoying. I would advicse against this, just like I'd advise against using type aliases.
> which happens to be checked against the compiler
as:
> the types aren't checked by the compiler, as my parent poster indicated.
(also type aliases are still checked. The check is less useful, but it's still a check)
> Why would any experienced programmer make such a blanket statement?
Because type annotations are less work than tests or documentation, don't suffer from falling out of sync like doc does, and because experience shows that haskell users typically find it useful to write types despite it not being necessary to compile. It would be surprising to skip types but invest in writing more complicated means of documenting and testing the software.
> 95% of annotations in the average haskell codebase are unneeded for compilation
Like type aliases. They are not distinct types, so the compiler doesn't (can't) check that you're using the "right" name.
That means most of the type annotations written exist because developers find them useful for documentation.
Doesn't it cause unintended API breaks because type changed from, say, a single type to union type, without changing any annotations?
If you have an API that exposes data to external systems, that may cause an integration breakage, indeed. That is why Haskell users like to use libs like Servant, which let you specify your API as a type. This way, if you change your system, your API has to change aswell.
Another benefit of Servant is that you can generate swagger and clients for other languages easily, which your users can use in their own build to get type safety too.
However, when sufficiently experienced, the type system also guides the developer into good program design. Type systems for me are more a design tool than shape checker.
The checker/inferencer helps me learn about assumptions I make about data in specific parts of my code, which allows me to remodel my data types to better fit my intent. This works cyclic until things just seem to ‘fit’.
Personally at least, this allows me to write far more complicated applications than I would ever be able to do without types. And it’s just very hard for me to imagine this wouldn’t be the case for any other developer.
Moreover, in OCaml it is idiomatic to use type inference everywhere except for interface files, so in practice you almost never annotate types.
This statement probably gets to the heart of the issue.
A static type system isn't something that just "does things", nor can we conduct a proper analysis when the dialectic is reduced to "static vs dynamic".
The first thing to realize is that there exists a spectrum in how much we encode in our type system. Even in weaker type systems like those of Go or Java, we can make illegal states unrepresentable, encode invariants, create typesafe abstractions, etc. Or we could just treat everything as object and not make any attempt to encode anything at all in the type system.
In the former case, the aphorism "if your code compiles, it probably works", is more true than in the latter case.
The next thing to realize is that the expressiveness of a type system places a ceiling on what can be usefully encoded in that type system. So in languages like Haskell, it's possible to get closer to the "if your code compiles, it probably works" ideal, than in a language with a weaker type system. But even in Haskell, it's possible to just write everything in IO and not make any attempt to encode your constraints in the type system.
So in that sense, a type system is just a tool. If wielded effectively, it lets us offload a lot of work to the compiler in a sort of symbiotic back-and-forth. And some languages give us better static typing tools, to make that symbiosis more effective. But if we're not using simpler type systems to their fullest, we're not likely to be pushing against their limits, making it a lot harder to justify more advanced type systems.
So "if your code compiles, it probably works" shouldn't be read as a statement of fact, but as an aspirational goal, one that we can move towards by more effective use of our tools.
You won't understand those aphorisms as long as think of "type annotation" as something useless, "tax-like". I know where you come from, in languages like Java it is a tax - you repeatedly write long-winded types in different places.
With type inference, that changes. You have a REPL (or IDE integration) that you can just ask for the inferred type as soon as you write a function. Then you look at it, you think about if it expresses what you want the function to do (that already catches some errors), you copy-and-paste it, make it a bit prettier, and bam, type annotation is done. It's even on an extra line, you don't have to attach it to arguments.
Or, you design with types ("test first style", think of the type annotation as a test). You first write down the type, then you write the function, and you see if the type checker agrees. If it doesn't, you probably found a bug. Or your idea of what the function should do was wrong (has also happened to me).
And everyone who uses that system does know that type checks are very dumb checks. They only help against dumb errors. But the majority of bugs are dumb bugs (at least the majority of my bugs) - misspellings, you forget to write out some part, etc. Those get caught. The hard bugs won't - that's what tests are for. (And automated testing is again simpler in the presence of types, see Quickcheck).
> and encourage the pursuit of bad abstractions like 18-arg functions
It never does. 18-arg functions are just unwieldy. If your function grows to that point, refactor (or rather, refactor long before that). And again, the type systems helps with refactoring.
(Yes, I know I won't convince you, this debate has been going on for at least 30 years. OTOH, it doesn't hurt to see the opinion on the other side).
I agree that it is an oversold fallacy and types get really clunky when pushed too far. But any worthy type system prevents lot of errors. This is not generally acclaimed by empirical studies claiming only 5% improvement because they are caught at compile time.
Other benefits are enough to use a sufficiently good static type system, by the way, even for people that think type systems aren't better than TDD.
* Better autocomplete
* Better error highlighting - no need to run code or tests, IDE can highlight instantly.
* No typos in variable names (can be achieved in some but not all dynamic languages)
* Performance of compiled codeOK, let's stretch that metaphor a bit. A powerful type system like Haskell's is really a way of both restricting which LEGO(tm) pieces you can use and designing custom new ones. So when you design and write software in truly idiomatic Haskell, after getting all your types lined up there is no way to make anything other than a fork.
They definitely do more than that in Haskell. You can define distinct types with the same "shape", and the `newtype` keyword specifically creates a new type that has the same "shape" as an existing one, but is nominally distinct.
This nominal typing, as it's called, allow you to express intent in types. Type checking then doesn't just check whether "shapes" line up, but also whether your intentions line up. If you express contradictory intentions, you probably made a mistake.
Practically speaking, if the only types ever used in a Haskell program are `Int`, `Float`, `Char` and nested lists of those, all the type checker can ever do is tell me when I miscount square brackets or forget a `fromIntegral`. Those programs never run on the first attempt.
Disagree. You can and should be using your type system to express your system design. If it's important that your utensil has at least 3 prongs, it should only be constructible by passing at least 3 prongs; that much is trivial to do even in Java, yet alone a language with a decent (i.e. ML-family) typesystem.
I find I would rarely reach for macros in Haskell, but that I sometimes would like to. Lisp macros do overlap with what you can achieve with Haskell things like typeclasses, laziness, monads and template Haskell, but the overlap is far from 100%.
The author of Hackett has a video about it: https://youtu.be/5QQdI3P7MdY
As the author says, the inbuilt standard library is hopelessly too small for modern requirements - even basics like string manipulation are only cursorily covered. And most of the third-party libraries out there are single-person projects, out of date, undocumented, or all three...
The language itself is still fantastic and, IMO, still superior to most other languages out there. But the lack of decent libraries is a major productivity killer.
I was thinking about mentioning that list, actually. Yes, there are libraries around for most common tasks. However, there are three things I'm not satisfied with:
1) I expect common tasks to be covered by the standard library. Third-party libraries are for specialised tasks.
2) There's often three or four mediocre libraries for the a given task instead of one really good one. Of course there are some "default libraries" (alexandria, bordeaux-threads, etc.) but too often you basically need to pick one at random and hope for the best. Best example: GUI libraries.
3) Most libraries are side-projects by individual developers or only have small teams. Which means: poor documentation and (in some cases) frequent breaking changes or random interruptions in development. The lack of documentation is not quite as bad as it sounds, because the source code tends to be highly readable. But still, the lack of polish and especially stability is not exactly trust-inspiring.
The CL ANSI standard really needs an overhaul to significantly expand the number of inbuilt functions. The fact that my 1990 edition of CLtL2 still adequately describes any modern CL implementation is a testament to the thoroughness of its language design, but also to the lack of change and expansion in the 30 years since.
Alternately, the CL community should develop its own semi-official stdlib - like the R community did with the tidyverse. A single package that you can install via Quicklisp that pulls in a curated list of high-quality libraries, ready for use. The CL standard might not come "batteries included" any time soon, but a "battery expansion pack" would still be a major improvement over "bring your own batteries" ;-)
That's an idea I would actually like to develop further :D Not sure HN is the right place though, so I moved it to r/Common_Lisp: https://www.reddit.com/r/Common_Lisp/comments/j7vd25/a_curat...
Every REPL form sent recompiles the running program, I don't want that fast loop to be force lagged by the program checking types
I'm more than happy for that to happen at edit time, independent of compile time outside of my application process
So I heavily encourage using clj-kondo to check for mistakes doing primitive type checking etc
To reiterate, I love static analysis but I also need control over how and when it runs, I like it in CI and during editing
Otherwise it comprises one of the best things about those old languages, (Smalltalk lisp etc) being able to dev your application by injecting code and recompiling while the app is still running
The feedback loop while coding a live image is hard to explain but anything that makes it slower is disastrous for me, I can lose focus so quick
When deving Clojure idiomatically partial recompile happens at a much greater frequency than most languages, compile time is very important
I'm not sure if what Haskell has is quite the same, abilities but you can hot-reload modules at runtime in GHCi, and FWIW, I've always found the type checker to be pretty zippy.
I agree, its pretty cool, and actually pretty fun.
This lets you do things like (stupid trivial example follows, Common Lisp not Clojure) write a function from inside out in the REPL, then move the code to source:
> (+ 1 2)
3
> (let ((x 1))
(+ x 2))
3
(defun add-two (x) ;; in a source file
(+ x 2))
> (add-two 1) ;; after evaluating/compiling the above
3
Imagine that these were more complicated things, like you're using a web API and getting JSON files and parsing them. You can develop it interactively, and as you verify that things are correct, you can migrate them to functions (and add tests to really do the verification part).Same. I have worked in both F# and Clojure profressionally. I love them both for different reasons.
As a fun thought experiment, I have often tried to force myself to choose between them, even though one doesn't really have to make a choice. Turns out that it's a tough choice to give one up over the other.
I tend to lean slightly towards Clojure at present, but then I'm using it a great deal at work.
However, if you're looking for a modern strongly typed, immutable functional language with type inference, then F# is really excellent.
I haven't used Haskell professionally and have only played with it but my feeling is that I prefer F# to Haskell because it has some "escape hatches" built in, in order to interop with C# and other .NET languages; So no IO Monad is required. It feels like more of a supporting structure and less of a straightjacket to me.
Any tips on how to have just a little bit of Clojure/JVM and f#/CLR in your life when day-to-day is on neither platform?
As far as Clojure goes, I usually use it for the JVM world, if I get a project and there are no other devs. Unfortunately it is almost impossible to convince Java devs to use Clojure and there are some limitations (Java interop for example) that makes it hard to sell. I am not even sure what would I do in your situation.
Essentially you can use it in most places you'd currently use bash scripts the install footprint is small and the startup time is fast
It's kind of like Clojure without the JVM, very easy to justify for one off bits and pieces
I always wonder: why use F# when you can use OCaml? OCaml has pretty decent third party libraries...
(Yes multicore support in F# would be a good reason but apart from that)
Having .NET CORE as a foundation means that whatever improvement the platform gets, F# gets.
This means that not only gets to interop with all the NET frameworks and such (good or bad), but also enjoy the massive performance improvements that's been going into CORE lately (compared to old NET).
Define slow. I never had an issues with the compilation speed of F#.
> why use F# when you can use OCaml?
Even though both of them are in the same family they could not be further away from each other. OCaml has many standard libraries more you can read here: https://discuss.ocaml.org/t/what-is-the-preferable-solution-...
F# has (almost) everything that is available in the CLR, which means tons of high quality libraries, ready to be used in production.
Another advantage is AWS support. Since C# is a tier one language in the cloud F# is also a tier one language. I am not sure where OCaml falls into in this space.
Other than these I am not sure why, but these are pretty big ones for us. I love OCaml and trying to use it whenever I can, which is not that much unfortunately.
I have looked at both without any prior experience in them and OCAML tooling is horrible. Also, with no support for unicode in Ocaml, F# is just much better in this regard - even if it lacks modules, functors, etc.
This is the old view. In 2020 OCaml tooling, if I may be brave to say so, is excellent. Check out the triumvirate of dune (build), opam (external dependencies) and ocaml-lsp (IDE smarts).
P.S. ocaml-lsp uses merlin in the backend (ignore that if you are not familiar with merlin).
For utf8 regexes, you can use Pcre in ocaml, which is faster than camomile regexes.
* Fail a project in Java, and you have a project failure. Fail a project in Haskell, and you have a Haskell failure.
* Management has a large veto power, and management usually favors technologies they are used to, or used to use themselves.
* Management also favors their ease of work with the tech. In practice, they often hold the belief that it's much easier to find java developers than it is to find haskell developers.
* Haskell suffers from a reputation of being "academic" and "hard to do real things with".
In practice, I've done great commercial projects in Haskell, and the technology was worth using. When done right, recruiting for haskell projects was much faster than recruiting for any mainstream language. On the flipside, it was sometimes hard to get adequate corporate stewardship for the language (for instance, managers/HR insisting they found no one for the job after publishing through the usual channels when you know half a dozen people at the local meetup that would jump ship if offered a position).
Other developers working in related areas are using Ruby and Python but my software runs a lot faster.
This is mostly because the majority of software developers only know Java and are averse to learning new languages. Also, companies chose Java at one point and are now taking next to no risks by introducing new languages (not even languages on the JVM). They can also just fire and rehire Java developers because there is a large pool of available developers that they can just plug-n-play, so to speak. They're not interested training people.
Part of that may be the continuing influence of PG: I both learnt Lisp and found HN because of his essays, and I suspect I'm not the only one.
But also, the HN community generally has an academic fondness for new and niche languages, so a language's popularity on HN is a poor proxy for its popularity in the business world.
(A last point: I think the diminished importance of Lisp in the last decades is less due to an inherent flaw of the language and more to what you might call "accidents of history" - particularly its close association with early attempts at AI.)
;; (ql:quickload "str")
(defun add-padding (txt &key padding)
(with-output-to-string (s)
(loop for line in (str:lines txt)
do (format s "~a~a~&" (str:repeat padding " ") line))))Im not sure his code is idiomatic SBCL but it could be written more succintly and functionally by most LISPers.
To the point on static/dynamic typing, I disagree. But its a discussion thats been going on for so long that Ive concluded its mostly a matter of taste. For Clojurians who like static typing, Spec packs a big punch without removing flexibility.
Still, Haskell is pretty great but Im more efficient in Clojure, Your mileage will vary.
The experience of a beginner is also important though, especially for language adoption, this might be one of the reasons why lisp flavours are not more popular.
I think my hate relationship toward algol syntax was so strong, sexp were an obvious answer from minute 0.
https://aphyr.com/posts/353-rewriting-the-technical-intervie...
That being said, having worked professionally in Lisp, the “un-lispy” macros like Loop are some of the worst parts of that language. They completely violate the otherwise standard-ish syntax of Lisp, are very poorly documented, impossible to memorize, and are really hard to debug despite the otherwise excellent tooling available for top tier implementations like SBCL. It was our standard practice to replace these macros with more verbose but idiomatic lisp whenever possible.
Also, macros are much harder to debug, especially when the macro expansion code itself crashes. The loop macro does a lot of work, which makes it a particularly annoying macro to fix when things go wrong.
(LOOP FOR I FROM 0 BELOW (LENGTH S) THEREIS FOR
CH = (CHAR S I) WHEN (< I 0) ALWAYS DO (RETURN CH))
*** - loop: illegal syntax near for in
(loop for i from 0 below (length s) thereis
for ch =
(char s i) when (< i 0) always do (return ch))https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node240.html#... states that "The thereis construct takes one form" so that will be your error. Not quite sure, as I don't know what you intend to express. when (< i 0) makes no sense as i is always positive.
http://www.gigamonkeys.com/book/loop-for-black-belts.html might be of help.
« New-York a des rats plus gros que des chiens »
I beg your pardon? Messing the word orders in French or German will definitely mess up the sentence.
The German would be something like “Im New York sind Ratten, die größter als Hunden sind”, and moving the words would make the sentence invalid – albeit probably understandable.