4 ms·
> Languages based on the λ-calculus make it really easy to “play back the code” in your head. This really depends on the expression.. I've seen behemoth expres
by karlicoss 6y ago
> Languages based on the λ-calculus make it really easy to “play back the code” in your head.
This really depends on the expression.. I've seen behemoth expressions such that you have no choice but write down intermediate results, or start explicitly breaking it down into sub-bindings so you could step through the code. And yes, you can usually step through subexpressions in the debugger, but if you have to debug anyway, it hardly matters that you can 'play back' the code in your head.
I also noticed there is a tendency in lispy languages to avoid introducing variable bindings just for the sake of naming the subexpression, because it often comes with a `(let ((name (subexpression))) rest-of-code)`, which also results in extra tabulation for the rest of the body. Compare with variable introduction in languages like python/js/c++/rust, etc., where such thing wouldn't cause touching the rest of the function/clause. I guess it's more of a 'functional language' artifact, e.g. in Haskell you'd probably have a similar issue.
The simple rules don't remove the complexity, the evaluation state has to live somewhere.
> I have never had a static type checker (regardless of how sophisticated it is) help me prevent anything more than an obvious error (which should be caught in testing anyway).
Err, really? Never dereferenced a null pointer, or tried using a list instead of a set? Static types are testing, it doesn't replace it, but massively saves mechanical work on writing dozens of dumb tests. Having tests is very important for documentation, regressions, end-to-end/integartion testing, but every test for something stupid as 'throws on passing null' is just a time drain (unless you're launching rockets or something of course).
Anyway, it's possible to have the best of both worlds by using gradual typing like mypy or JS flow -- you get all the runtime benefits (flexibility/ability to temporary violate invariants) and you gradually harden code which makes sense to type. I really wish Elisp (the lisp I'm mostly dealing with) had some sort of types, at least for simple things list 'associative list', 'string', 'function reference', 'nullable thing', this would massively save me time on catching bugs. It has runtime type checks for `defcustom` things, but never seen type checking anywhere else (except for occasional runtime asserts).
> The biggest advantage of this form of syntax is a form of minimalism — you don’t need spurious syntactic constructs to convey concepts.
> call these things macros, or syntactic extensions. In other words, you can extend the syntax of your language to introduce new abstractions.
I find these two sentences a bit contradictory ;) Anyway, I personally like macros, if used sparingly it can really help.
One great upside of s-expressions for me is that you can do some cool things like 'find and replace' for whole subexpressions (for monkey patching third party code, for example). I use `el-patch` [0] in my emacs config and `advice-patch` for surgically changing the default behaviors of some org-mode functions to compile my blog [1].
That said a similar sort of thing is possible, for example, in python with `patchy` [2], and perhaps many other languages? But I guess it's not as organic as in lisps, e.g. `advice-patch` implementation is less than 100 LOC, whereas in case of python you have to rely on existing heavy lifting done by `ast` module.
As of simple syntax, it really gets in the way sometimes, e.g. I'm always annoyed by constant quoting in Elisp because the same type of brackets (only `()`) is used. In comparison, in Clojure it's much more readable with (), [], {} (and more).
> Lisp is not an interpreted language. It is not slow
A bit of nitpicking, but... which Lisp? :) Anyway, these days it's often meaningless to say 'slow' without having a specific workload in mind and having done benchmarking; hardware improvements make it very hard to reason about.
> all implementations come with lots and lots of levers to tweak performance for most programs. In some cases the programs might need assistance from faster languages like C and C++ because they are closer to the hardware, but with faster hardware, even that difference is becoming irrelevant.
Well, this is true of most languages.
[0] https://github.com/raxod502/el-patch#el-patch https://github.com/raxod502/el-patch#el-patch
[1] https://github.com/karlicoss/beepb00p/blob/a4fd7cb95e1705412d68c568ea13f2b0a831527f/exobrain/src/publish.el#L45-L74 https://github.com/karlicoss/beepb00p/blob/a4fd7cb95e1705412...
[2] https://github.com/adamchainz/patchy#patchy https://github.com/adamchainz/patchy#patchy
- zelphirkalt 6y ago> I also noticed there is a tendency in lispy languages to avoid introducing variable bindings just for the sake of naming the subexpression, because it often comes with a `(let ((name (subexpression))) rest-of-code)`, which also results in extra tabulation for the rest of the body. Compare with variable introduction in languages like python/js/c++/rust, etc., where such thing wouldn't cause touching the rest of the function/clause. I guess it's more of a 'functional language' artifact, e.g. in Haskell you'd probably have a similar issue. I have found myself thinking about "Should I really introduce another let form for naming these things I am going to use at the next procedure call, to make it more readable? Or would it make the code less readable, because of the additional indent?". So there is something to this point. Right now, while not writing code, I would say: The indentation make the scope of the bindings clear. It tells you exactly, until where a binding will be defined. It also allows you to shadow a binding from an outer scope for the purpose of using it in the inner scope. However shadowing should be used with care, because it can also get in the way of readability, if used wrongly. It can however also increase readability, if used well. In a language like Python, you can only write things at the same level of indentation. It does not make it clear until where these bindings might possibly be used. The scopes are not as separated as in Lispy languages with the let form. That inherently makes it not as readable, I think. "The rest of the function" is not a very precise scope specification. Many bindings might only be used for one or a few subexpressions and not everywhere until the function ends. This only makes sense though, if the code has side effects. Otherwise there will be no difference in where the bindings still exist, because what in Python is the rest of the "function", will be the inner parts of the expression of a procedure in Lispy languages. I have found myself looking for a thing like let in Python though. I have sometimes thought: "Hey, can't I use a with-block for making a temporary binding?", but that has never worked for me, because I think you need to make a context manager for that? It has never materialized in my style of Python code. (I know Hy, but until it can offer me TCO and lambdas and let forms like Scheme, I am not sure I would like to use it.) EDIT: Ah and not to forget about let* which can often reduce the amount of indentation a lot. Sometimes I find it good to make use of it.
- Abhinav2000 6y agoWhen would shadow binding help create more readable code? (I agree with everything else in your post)