<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hacker News: sparkie</title><link>https://news.ycombinator.com/user?id=sparkie</link><description>Hacker News RSS</description><docs>https://hnrss.org/</docs><generator>hnrss v2.1.1</generator><lastBuildDate>Sun, 23 Aug 2026 22:20:14 +0000</lastBuildDate><atom:link href="https://hnrss.org/user?id=sparkie" rel="self" type="application/rss+xml"></atom:link><item><title><![CDATA[New comment by sparkie in "Tail-call optimization in C is relatively recent (2025)"]]></title><description><![CDATA[
<p>That's not entirely true, but it's a valid reason to prefer using musttail.<p>`register` is a hint if you don't specify which register you want to use - however, if you specify the register it will clobber it.<p><pre><code>    noinline void bar() 
    {
        register void *parent __asm__("r10");
        ...
    }
</code></pre>
You can also use GCCs extended asm syntax to clobber a register for specific portions of code - such as the start of a function where you expect a register to have been given a value from the caller just before the call. Use `volatile` to prevent the compiler from making certain assumptions that might remove or reorder the instruction - as long as it is at the top it should execute immediately after the function prelude and before any of the function body.<p><pre><code>    noinline void bar() 
    {
        void *volatile parent;
        // set parent = %r10 before anything else.
        asm volatile ("mov{q}\t{%%r10, %0|%0, r10}" : "=r"(parent) : : "r10");
        ...
    }
</code></pre>
Note that this will probably be less efficient than the former example, but maybe useful where you want to limit the scope in which `r10` is clobbered.<p>In both cases you would set the register immediately before making the call, again using `volatile`. Since `r10` is not used by a typical call in SYSV - it's the static chain pointer in the SYSV convention, but otherwise usable as a GP register, a call will not overwrite it.<p><pre><code>    void foo()
    {
        struct foo_frame {
            int x;
        } locals = { 
            .x = 999
        };

        // Set `r10` to our function's local frame
        asm volatile("mov{q}\t{%0, %%r10|r10, %0}" : : "r"(&locals) : "r10")
        
        bar();
    }
</code></pre>
That's pretty ugly but we can write a few macros to implement it more tersely - we can use this to have efficient closures in C without requiring an executable stack. (There's also `__builtin_call_with_static_chain`, but I've found it more troublesome to use than the manual way).<p>Demo: <a href="https://godbolt.org/z/cM9d8e1r5" rel="nofollow">https://godbolt.org/z/cM9d8e1r5</a><p>For other registers which are part of the regular calling convention, we might be able to clobber them if they wouldn't normally be used for the call. Eg, if our function takes regular 2 arguments, they would be in `rdi` and `rsi` - so we could use `rdx`, `rcx`, `r8`, `r9` like the above, but if our function took 6 or more regular arguments we wouldn't be able to use any of these in this way. If we wanted a custom calling convention we could just make all functions have zero-arguments and perform all of the setting and capturing ourself - which gives us more control than using [[musttail]] - though less portable, and may prevent optimizations the compiler could otherwise make.</p>
]]></description><pubDate>Tue, 11 Aug 2026 00:35:48 +0000</pubDate><link>https://news.ycombinator.com/item?id=49251781</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=49251781</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=49251781</guid></item><item><title><![CDATA[New comment by sparkie in "Tail-call optimization in C is relatively recent (2025)"]]></title><description><![CDATA[
<p>> What practical patterns are enabled by TCO in C?<p>Continuation Passing Style - an important construction for interpreters, but which is also useful for compilers as it's a nice way to do control flow analysis, data flow analysis and more.<p>The missing feature is closures - functions which capture values from their static environment, which are basically needed to make CPS useful. GCC has nested functions, but they cannot capture without making the stack executable, which is terrible. There's a proposal[1] to get closures into C, but at present you need to simulate the capturing yourself, which is cumbersome, but can be done efficiently.<p>[1]:<a href="https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Functions%20with%20Data%20-%20Closures%20in%20C.html#design-capture.functions" rel="nofollow">https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Functio...</a></p>
]]></description><pubDate>Mon, 10 Aug 2026 22:29:02 +0000</pubDate><link>https://news.ycombinator.com/item?id=49250746</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=49250746</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=49250746</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>That may be true, but it may also mean you utilize more memory than you need to. If you aren't shrinking the array when you no longer need previously allocated capacity then you're wasting memory. You could end up with an array of 10 elements and an allocation of 2^10.<p>The capacity as bit_ceil(len) ensures that at most, half of the allocated space is wasted - excess space is O(n).</p>
]]></description><pubDate>Sat, 13 Jun 2026 08:12:07 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514768</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514768</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514768</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>I don't think it's that rare. I've been using the technique for years, and I've seen it done in other work. Bagwell's VList[1] for example uses the equivalent of `bit_ceil` to determine the size of each block without having to store it - and there are earlier works based on the same trick. RAOTS, which is referenced by the VList, mentions using the technique, but itself uses a slightly more complex trick where we can calculate the size of a block based on the approx square root of the length.<p>You can use the trick if the array can shrink as long as you always shrink the allocation when length goes below the next power of 2 not greater than len (which may make use of stdc_bit_floor).<p>[1]:<a href="https://cl-pdx.com/static/techlists.pdf" rel="nofollow">https://cl-pdx.com/static/techlists.pdf</a></p>
]]></description><pubDate>Sat, 13 Jun 2026 08:07:24 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514727</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514727</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514727</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>If you use a struct with a `void*`, you also need to specify the type on usage, where here it's done with `typeof`.</p>
]]></description><pubDate>Sat, 13 Jun 2026 07:25:48 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514430</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514430</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514430</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>In C23 this approach is nice, but in older versions of C we end up with awful macros where we need to define the structure before we use it.<p><pre><code>    #define Array(T) struct array_##T
    #define DEFINE_ARRAY(T) struct array_##T { size_t len; T *elems; }
    
    DEFINE_ARRAY(int);
    Array(int) foo;
    Array(int) bar;
</code></pre>
C23 has relaxed rules for redefining the same struct, so we can avoid having to create the struct up front.<p><pre><code>    #define Array(T) struct array_##T { size_t len; T *elems; }

    Array(int) foo;
    Array(int) bar;</code></pre></p>
]]></description><pubDate>Sat, 13 Jun 2026 07:18:21 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514376</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514376</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514376</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>The reason the struct is avoided here is so the array can be typed to its element type (rather than casting to and from `void*`).<p>With a struct we would need one struct for each element type - at least prior to C23 which provides a better approach where we can declare the same struct multiple times in a translation unit.<p><pre><code>    #define Array(T) struct array_##T { size_t len; T *elems; }
</code></pre>
We can use `Array(int)` in multiple places in the same TU - but in C11 or earlier, this is an error.</p>
]]></description><pubDate>Sat, 13 Jun 2026 07:14:29 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514347</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514347</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514347</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>The concept of not storing capacity isn't silly. If you need to reserve space then it's not the appropriate structure, but it's otherwise fine.<p>However, using an 2-element array to avoid using a struct is silly.</p>
]]></description><pubDate>Sat, 13 Jun 2026 07:07:47 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514302</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514302</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514302</guid></item><item><title><![CDATA[New comment by sparkie in "A generic dynamic array in C that stores no capacity and needs no struct"]]></title><description><![CDATA[
<p>Really? It's been done plenty and I thought was quite common knowledge. Some of the <stdbit.h> provided functions are basically for this purpose.<p>stdc_bit_ceil(len) gets the smallest power of 2 not less than len, which is our capacity. This is usually implemented with a clz instruction.<p>stdc_has_single_bit(len) determines if it's a power of 2 - typically implemented with a popcount instruction (popcount(len)==1).<p>The approach isn't used in older (90s and earlier) texts because hardware support for popcount/clz wasn't commonplace and the cost to do it in software wasn't worth it, but it is mentioned in some texts.</p>
]]></description><pubDate>Sat, 13 Jun 2026 06:55:15 +0000</pubDate><link>https://news.ycombinator.com/item?id=48514205</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48514205</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48514205</guid></item><item><title><![CDATA[New comment by sparkie in "Bijou64: A variable-length integer encoding"]]></title><description><![CDATA[
<p>Bitcoin has a variable width encoding (`CompactSize`), but it doesn't prevent overlong encodings - however there are various canonicalization rules in the Bitcoin protocol to require minimal encoding.</p>
]]></description><pubDate>Sat, 30 May 2026 20:30:05 +0000</pubDate><link>https://news.ycombinator.com/item?id=48340307</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=48340307</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=48340307</guid></item><item><title><![CDATA[New comment by sparkie in "Lib0xc: A set of C standard library-adjacent APIs for safer systems programming"]]></title><description><![CDATA[
<p>The C charter has a rule of "no invention".<p>Anything needs to be demonstrated and used in practice before being included in the standard. The standard is only meant to codify existing practices, not introduce new ideas.<p>It's up to compiler developers to ship first, standardize later.</p>
]]></description><pubDate>Sat, 02 May 2026 01:50:03 +0000</pubDate><link>https://news.ycombinator.com/item?id=47982536</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47982536</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47982536</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>Lisps are expression based languages, but not pure. It's easy to mistake it as "like most other languages", but it's not quite the same - everything is an expression and returns a result. There are no "statements".<p>They appear procedural because of syntax sugar - ie, the body of a function is basically implicitly wrapped in (progn ...), (begin ...), ($sequence ...), etc - which are all equivalent expression forms which evaluate their sub-expressions in order and return the result of the last one.<p><pre><code>   (progn a b c)      ;; CommonLisp
   (begin a b c)      ;; Scheme
   ($sequence a b c)  ;; Kernel

   ;; evaluate a, then b, then c, 
   ;; ignore the results of evaluating a and b 
   ;; return the result of evaluating c.
</code></pre>
So when you see:<p><pre><code>   (define (foo)
       (expr1)
       (expr2)
       (expr3))
</code></pre>
If we desugar, it would be<p><pre><code>    (define foo (lambda () (begin (expr1) (expr3) (expr3))))
</code></pre>
We get behavior that looks just like other procedural languages (without a "return" keyword) - but everything is still an expression.<p>A similarity is the comma operator in C. Imagine you didn't write statements but the body of your C functions was entirely chains of comma operators.<p>CommonLisp has a couple of other useful related forms - prog1 and prog2. They still evaluate their sub-expressions in order, but prog1 returns the result of evaluating the first expression, and prog2 returns the result of the second expression.<p><pre><code>    (define (foo) (prog1 (expr1) (expr2) (expr3))
    (foo)
 
    ;; evaluates expr1, then expr2, then expr3
    ;; returns the result of evaluating expr1
    ;; ignores the results of evaluating expr2 and expr3</code></pre></p>
]]></description><pubDate>Thu, 30 Apr 2026 19:47:51 +0000</pubDate><link>https://news.ycombinator.com/item?id=47967355</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47967355</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47967355</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>Operatives are based on FEXPRS from older lisps - they're basically a function-like form, but where the operands are not implicitly reduce at the time of call.<p><pre><code>    (foo (+ 2 3) (* 3 4))
    ($bar (+ 2 3) (* 3 4))
</code></pre>
`foo` is a function, when it is combined with the <i>arguments</i>, it receives the values 5 and 7.<p>`$bar` however, receives its operands verbatim. It receives (+ 2 3) as its first operand and (* 3 4) as its second - unevaluated.<p>The operative/FEXPR body decides how to evaluate the operands - if at all.<p>The difference between an operative/FEXPR and a macro is that macros are second-class objects which must appear in their own name - we cannot assign them to variables, pass them or return them from functions. Operatives and FEXPRs are first-class objects that can be treated like any other.<p>The difference between FEXPRs and Operatives is to do with scoping and environments. FEXPRs were around before Scheme - when Lisps were dynamically scoped. This meant we could have unpredictable behavior and so called "spooky action at distance". They were problematic and basically abandoned almost entirely in the 1980s.<p>Shutt introduced Operatives as a more hygienic version - based on statically scoped Scheme. Instead of the operative being able to mutate the dynamic environment arbitrarily, there are limitations. The first part of this is that environments are made into first-class objects - so we can assign them to a symbol and pass them around. The final part is that an operative receives a reference to the dynamic environment of its caller - which we bind to a symbol using the operative constructor, `$vau`.<p><pre><code>    ($vau (operands) dynamic-env . body)
</code></pre>
Compare to:<p><pre><code>    ($lambda (arguments) . body)
</code></pre>
So operatives are called in the same way a function is called - but the operands are not reduced, and the environment is passed implicitly.<p>The body can decide to evaluate the operands using the environment of the caller - essentially behaving as if the caller had evaluated them<p><pre><code>    (eval operands dynamic-env)

</code></pre>
But it can chose other evaluation strategies for the operands - such as evaluating them in a custom created environment which we can make with (make-environment) or ($bindings->environment).<p>This also allows the operative to mutate the environment of its callee - but <i>only</i> the locals of that environment. The parent environments cannot be mutated through the reference `dynamic-env`.<p>Technically, `$lambda` is not primitive in Kernel - though it is the main constructor of <i>applicatives</i> (functions) - the primitive constructor is called `wrap` - and it takes another combiner (an operative or applicative) as its parameter. Wrapping a combiner simply forces the evaluation of its arguments when called - so functions are just wrappers around operatives - and the underlying operative of any function can be extracted with `unwrap`.<p>There's a lot more to them. They're conceptually quite simple in terms of implementation, but they have enormous potential use cases that are unexplored.<p>Read more on the Kernel page[1]. In particular, the Kernel report[2]. There's also a formal calculus describing them, called the vau calculus[3].<p>[1]:<a href="https://web.cs.wpi.edu/~jshutt/kernel.html" rel="nofollow">https://web.cs.wpi.edu/~jshutt/kernel.html</a><p>[2]:<a href="https://ftp.cs.wpi.edu/pub/techreports/pdf/05-07.pdf" rel="nofollow">https://ftp.cs.wpi.edu/pub/techreports/pdf/05-07.pdf</a><p>[3]:<a href="https://web.archive.org/web/20150224035948/http://www.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unrestricted/jshutt.pdf" rel="nofollow">https://web.archive.org/web/20150224035948/http://www.wpi.ed...</a></p>
]]></description><pubDate>Thu, 30 Apr 2026 19:38:37 +0000</pubDate><link>https://news.ycombinator.com/item?id=47967243</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47967243</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47967243</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>There's no "external representation" for environments. In klisp it will just print:<p><pre><code>    [#environment]
</code></pre>
The environment type is encapsulated, so it doesn't give you very useful debug information.<p>Perhaps having `@` produce an environment is the wrong approach and we should just produce an association list instead - then move `$bindings->environment` into the `?` operative to enable querying.</p>
]]></description><pubDate>Thu, 30 Apr 2026 15:04:10 +0000</pubDate><link>https://news.ycombinator.com/item?id=47963608</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47963608</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47963608</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>I use klisp[1] and bronze-age-lisp[2] mostly for testing, as they're the closest to a feature complete implementation of the Kernel Report.<p>I've written a number of less complete interpreters over the years. I currently have a long-running side-project to provide a more complete, highly optimized implementation for x86_64.<p>[1]:<a href="https://github.com/dbohdan/klisp" rel="nofollow">https://github.com/dbohdan/klisp</a><p>[2]:<a href="https://github.com/ghosthamlet/bronze-age-lisp" rel="nofollow">https://github.com/ghosthamlet/bronze-age-lisp</a></p>
]]></description><pubDate>Thu, 30 Apr 2026 10:33:19 +0000</pubDate><link>https://news.ycombinator.com/item?id=47960520</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47960520</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47960520</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>I understand the use case, but Scheme macros never felt intuitive to me. I think it may be the quotation more than anything that I dislike - though I also dislike that they're second class (which was the key thing which led me to Kernel).<p>I use C preprocessor macros extensively and don't have the typical dislike for them that many people have - though I clearly understand their limitations and the advantage Scheme macros have over them.<p>Since learning Kernel, the boundary of "compile time" and "runtime" is more blurry - I can write operatives which behave somewhat like a macro, and I do more "multi-stage" programming, where one operative optimizes its argument to produce something more efficient which is later evaluated - though there are still limitations due to the inability to fully compile Kernel.<p>As one example, I've used a kind of operative I call a "template", which evaluates its free symbols ahead of time but doesn't actually evaluate the body. When we later apply the some operands it replaces the bound symbols with the operands, looking up any symbols to produce an expression which we don't need to immediately evaluate either - but this expression has all symbols fully resolved. This is somewhere between a macro and regular operative.<p>Consider:<p><pre><code>    ($define! z 10)

    ($define! @add-z
        ($template (x y)
            (+ x y z)))
</code></pre>
In this template `x` and `y` are bound variables and `+` and `z` are free. The template resolves the free symbols and returns an operative expecting 2 operands, effectively providing an operative with the body:<p><pre><code>    ([#applicative: +] x y 10)
</code></pre>
When we call the template with the two operands, it resolves any symbols in the arguments and returns the full expression with no symbols present, but it doesn't evaluate the expression yet.<p><pre><code>    > ($let ((x 9)
             (y 7))
          (@add-z (* x 3) (- y 13)))
    ([#applicative: +] ([#applicative: *] 9 3) ([#applicative: -] 7 13) 10)
</code></pre>
When we decide to evaluate the expression, no symbol lookup is necessary - it can perform the operation rather quickly, despite the slow interpretation.<p>---<p>The $template form above isn't too difficult to implement. I've iterated several forms of this - some which only partially resolved the bound symbols, but lost them in a RAID failure. An earlier version which has some issues I still have because I put it online:<p><pre><code>    ($provide! ($template)
        ($define! $resolve-free-symbols
            ($vau (params expr) env
                ($cond
                    ((null? expr) ())
                    ((pair? expr)
                        (cons (apply (wrap $resolve-free-symbols) 
                                     (list params (car expr)) 
                                     env)
                              (apply (wrap $resolve-free-symbols) 
                                     (list params (cdr expr)) 
                                     env)))
                    ((symbol? expr)
                        ($if (member? expr params)
                             expr
                             (eval expr env)))
                    (#t expr))))

        ($define! $resolve-bound-symbols
            ($vau (params expr) env
                ($cond
                    ((null? expr) ())
                    ((pair? expr)
                        (cons (apply (wrap $resolve-bound-symbols)
                                     (list params (car expr))
                                     env)
                              (apply (wrap $resolve-bound-symbols)
                                     (list params (cdr expr))
                                     env)))
                    ((symbol? expr)
                        ($if (member? expr params)
                             (eval expr env)
                             expr))
                    (#t expr))))

        ($define! zip
            ($lambda (fst snd)
                ($cond
                    (($and? (null? fst) (null? snd)) ())
                    (($and? (pair? fst) (pair? snd))
                        (cons (list (car fst)
                                    (list* (($vau #ignore #ignore list)) (car snd)))
                              (zip (cdr fst) (cdr snd)))))))

        ($define! $template
            ($vau (params body) senv
                ($let ((newbody 
                        (eval (list $resolve-free-symbols params body) senv)))
                    ($vau args denv
                        (eval (list $resolve-bound-symbols params newbody)
                              (eval (list* $bindings->environment
                                           (zip params args))
                                    denv))))))) 

</code></pre>
---<p>At present the best interpreter is klisp, and the fastest is bronze-age-lisp, which uses klisp - with parts of hand-written 32-bit x86 assembly.<p>I've been working on a faster interpreter for a number of years as a side project, optimized for x86_64 with some parts C and some parts assembly. It has diverged in some parts from the Kernel report, but still retains what I see are the key ingredients.<p>My modified Kernel has optional types, and we have operatives to `$typecheck` complex expressions ahead of evaluating them. I intend to go all in on the "multi-stage" aspect and have operatives to JIT-compile expressions in a manner similar to the above template.</p>
]]></description><pubDate>Thu, 30 Apr 2026 10:28:41 +0000</pubDate><link>https://news.ycombinator.com/item?id=47960496</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47960496</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47960496</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>Operatives do that for me, better than macros. Parent is correct that macros are compile time, which gives them a performance advantage over operatives - but IMO, they're not better ergonomically. I find operatives simpler, cleaner and more powerful.</p>
]]></description><pubDate>Thu, 30 Apr 2026 09:51:32 +0000</pubDate><link>https://news.ycombinator.com/item?id=47960237</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47960237</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47960237</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>In Kernel I would use something like this:<p><pre><code>    true        #t
    false       #f
    null        ()
    [...]       (& ...)
    "k" : v     (: k v)
    {...}       (@ ...)  
</code></pre>
Where &, :, @ are defined as:<p><pre><code>    ($define! &
        ($lambda args (cons list args)))

    ($define! : 
        ($vau (key value) env
            (list key (eval value env))))
            
    ($define! @ 
        (wrap 
            ($vau kvpairs env 
                (eval (list* $bindings->environment kvpairs) env))))
</code></pre>
Using the "person" example from the JSON/syntax section on Wikipedia:<p><pre><code>    ($define! person
        (@
            (: first_name "John")
            (: last_name "Smith")
            (: is_alive #t)
            (: age 27)
            (: address 
                (@
                    (: street_address "21 2nd Street")
                    (: city "New York")
                    (: state "NY")
                    (: postal_code "10021-3100")))
            (: phone_numbers
                (& (@ (: type "home") (: number "212 555-1234"))
                   (@ (: type "office") (: number "646 555-4567"))))
            (: children
                (& "Catherine" "Thomas" "Trevor"))
            (: spouse ())))
</code></pre>
I would then define `?`<p><pre><code>    ($define! ? $remote-eval)
</code></pre>
Now we can query the object.<p><pre><code>    > (? age person)
    27

    > (? postal_code (? address person))
    "10021-3100"

    > (car (? children person))
    "Catherine"

    > (cdr (? children person))
    ("Thomas" "Trevor")

    > (? type (cadr (? phone_numbers person)))
    "office"

    > (? number (car (? phone_numbers person)))
    "212 555-1234"

    > ($define! full_name ($lambda (p) (string-append (? first_name p) " " (? last_name p))))
    > (full_name person)
    "John Smith"</code></pre></p>
]]></description><pubDate>Thu, 30 Apr 2026 03:37:49 +0000</pubDate><link>https://news.ycombinator.com/item?id=47957789</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47957789</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47957789</guid></item><item><title><![CDATA[New comment by sparkie in "Why I still reach for Lisp and Scheme instead of Haskell"]]></title><description><![CDATA[
<p>When I learned Scheme, I liked the language but strongly disliked macros and quotation. I'd only been using it a short while and when I searched for solutions to a few problems these "fexpr" things kept appearing up, which i didn't understand, and this "Kernel" language. I decided to learn it since "fexprs" were apparently the solution to several of my problems. This wasn't easy at first - I had to read the Kernel Report several times, but I ended up finding it way more intuitive than using macros and quotes.<p>I've not written a Scheme macro since. I've written hundreds of Kernel operatives though.<p>I was also a typoholic previously, but am in remission now thanks to Kernel.<p><a href="https://web.cs.wpi.edu/~jshutt/kernel.html" rel="nofollow">https://web.cs.wpi.edu/~jshutt/kernel.html</a></p>
]]></description><pubDate>Thu, 30 Apr 2026 02:04:40 +0000</pubDate><link>https://news.ycombinator.com/item?id=47957232</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47957232</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47957232</guid></item><item><title><![CDATA[New comment by sparkie in "All elementary functions from a single binary operator"]]></title><description><![CDATA[
<p>It's potentially useful for computer algebra with complex numbers - we might be able to simplify formulas using non-standard methods, but instead via pattern matching. We might use this to represent <i>exact</i> numbers internally, and only produce an inexact result when we later reduce the expression.<p>Consider it a bit like a "church encoding" for complex numbers. I'll try to demonstrate with an S-expression representation.<p>---<p>A small primer if you're not familiar. S-expressions are basically atoms (symbols/numbers etc), pairs, or null.<p><pre><code>    S = <symbol>
      | <number>
      | (S . S)      ;; aka pair
      | ()           ;; aka null
    </code></pre>
There's some syntax sugar for right chains of pairs to form lists:<p><pre><code>    (a b c)          == (a . (b . (c . ()))   ;; a proper list
    (a b . c)        == (a . (b . c))         ;; an improper list
    (#0=(a b c) #0#) == ((a b c) (a b c))     ;; a list with a repeated sublist using a reference
</code></pre>
---<p>So, we have a function `eml(x, y) and a constant `1`. `x` and `y` are symbols.<p>Lets say we're going to replace `eml` with an infix operator `.`, and replace the unit 1 with `()`.<p><pre><code>    C = <symbol>
      | <number>
      | (C . C)      ;; eml
      | ()           ;; 1
</code></pre>
We have basically the same context-free structure - we can encode complex numbers as lists. Let's define ourselves a couple of symbols for use in the examples:<p><pre><code>    ($define x (string->symbol "x"))
    ($define y (string->symbol "y"))
</code></pre>
And now we can define the `eml` function as an alias for `cons`.<p><pre><code>    ($define! eml cons)

    (eml x y)
    ;; Output: (x . y)
</code></pre>
We can now write a bunch of functions which construct trees, representing the operations they perform. We use only `eml` or previously defined functions to construct each tree:<p><pre><code>    ;; e^x

        ($define! exp     ($lambda (x) (eml x ())))
        
        (exp x)
        ;; Output: (x)
        ;; Note: (x) is syntax sugar for (x . ())

    ;; Euler's number `e`

        ($define! c:e     (exp ()))
        
        c:e          
        ;; Output: (())
        ;; Note: (()) is syntax sugar for (() . ())

    ;; exp(1) - ln(x)

        ($define! e1ml    ($lambda (x) (eml () x)))
        
        (e1ml x) 
        ;; Output: (() . x)

    ;; ln(x)

        ($define! ln      ($lambda (x) (e1ml (exp (e1ml x)))))
        
        (ln x)
        ;; Output: (() (() . x))

    ;; Zero

        ($define! c:0      (ln ()))
        
        c:0
        ;; Output: (() (()))

    ;; -infinity

        ($define! c:-inf   (ln 0))
        
        c:-inf
        ;; Output: (() (() () (())))

    ;; -x
        
        ($define! neg      ($lambda (x) (eml c:-inf (exp x))))

        (neg x)
        ;; Output: ((() (() () (()))) x)
        
    ;; +infinity
    
        ($define! c:+inf   (neg c:-inf))
        
        c:+inf
        ;; Output: (#0=(() (() () (()))) #0#)
        
    ;; 1/x
    
        ($define! recip    ($lambda (x) (exp (eml c:-inf x))))
        
        (recip x)
        ;; Output: (((() (() () (()))) . x))
  
    ;; x - y
    
        ($define! sub      ($lambda (x y) (eml (ln x) (exp y))))
    
        (sub x y)
        ;; Output: ((() (() . x)) y)
    
    ;; x + y
    
        ($define! add      ($lambda (x y) (sub x (neg y))))
    
        (add x y)
        ;; Output: ((() (() . x)) ((() (() () (()))) y))
    
    ;; x * y
    
        ($define! mul      ($lambda (x y) (exp (add (ln x) (exp (neg y))))))
        
        (mul x y)
        ;; Output: (((() (() () (() . x))) (#0=(() (() () (()))) ((#0# y)))))
        
    ;; x / y
    
        ($define! div      ($lambda (x y) (exp (sub (ln x) (ln y)))))
        
        (div x y)
        ;; Output: (((() (() () (() . x))) (() (() . y))))
        
    ;; x^y
    
        ($define! pow      ($lambda (x y) (exp (mul x (ln y)))))
  
        (pow x y)
        ;; Output: ((((() (() () (() . x))) (#0=(() (() () (()))) ((#0# (() (() . y))))))))
  </code></pre>
I'll stop there, but we continue for implementing all the trig, pi, etc using the same approach.<p>So basically, we have a way of constructing trees based on `eml`<p>Next, we pattern match. For example, to pattern match over addition, extract the `x` and `y` values, we can use:<p><pre><code>    ($define! perform-addition
        ($lambda (add-expr)
            ($let ((((() (() . x)) ((() (() () (()))) y)) add-expr))
                (+ x y))))  

    ;; Note, + is provided by the language to perform addition of complex numbers

    (perform-addition (add 256 512))
    ;; Output: 768
</code></pre>
So we didn't need to actually compute any `exp(x)` or `ln(y)` to perform this addition - we just needed to pattern match over the tree, which in this case the language does for us via deconstructing `$let`.<p>We can simplify the defintion of perform-addition by expanding the parameters of a call to `add` as the arguments to the function:<p><pre><code>    ($define! $let-lambda
        ($vau (expr . body) env
            ($let ((params (eval expr env)))
                (wrap (eval (list* $vau (list params) #ignore body) env)))))
                
    ($define! perform-addition
        ($let-lambda (add x y)
            (+ x y)))

    ($define! perform-subtraction
        ($let-lambda (sub x y)
            (- x y)))


    ($define! sub-expr (sub 256 512))
    ;; Output: #inert
    sub-expr
    ;; Output: ((() (() . 256)) 512)

    (perform-subtraction sub-expr)
    ;; Output: -256

</code></pre>
There's a bit more work involved for a full pattern matcher which will take some arbitrary `expr` and perform the relevant computation. I'm still working on that.<p>Examples are in the Kernel programming language, tested using klisp[1]<p>[1]:<a href="https://github.com/dbohdan/klisp" rel="nofollow">https://github.com/dbohdan/klisp</a></p>
]]></description><pubDate>Wed, 15 Apr 2026 16:04:19 +0000</pubDate><link>https://news.ycombinator.com/item?id=47781063</link><dc:creator>sparkie</dc:creator><comments>https://news.ycombinator.com/item?id=47781063</comments><guid isPermaLink="false">https://news.ycombinator.com/item?id=47781063</guid></item></channel></rss>