Hacker News new | past | comments | ask | show | jobs | submit | JoelMcCracken's comments login

I use my boox max lumi as a secondary display daily for working in emacs. The eink is great for text/terminal use, the only issue I have is when i sometimes need to do any kind of mouse work (which, is basically never, when I use it for what I said above).

What I really want is a low power linux laptop that is not entirely without CPU/memory power, so I can program some simple things on it. I don't mind if it has _less_ power, I can use ssh for anything that is overly cpu-hungry.

Ive seen several devices that seem like they might suit my need, but I look at them for long enough and just won't pull the trigger. Either it seems overly much like a walled garden (like, I can program on the device, but it doesn't seem like a suitable spot to write blog posts in emacs for my blog or whatever), or its just too underpowered and I'm sure that 99% of the tools I use already won't work on it.

I wish I had the EE knowledge/confidence to start hacking on this kind of thing. I think its very doable; I was just looking at e.g. https://www.waveshare.com/product/displays/e-paper/epaper-1/...

which is just cheap enough that I could see myself risking buying it without being sure that it will work with my other choices.

Nowadays, I feel like I should be able to run most of what I want on an android device that is built for power, and it should have a fairly long lasting battery because of its design; attach a trackpad, keyboard, and eink display, and my perfect device is here. I don't care if its not the thinnest device in the universe, a swappable battery (or, just load the thing with extra batteries) plus perhaps a portable solar charger would be amazing.


Some other comment mentioned the pinenote which was shown on https://fosstodon.org/@carbonatedcaffeine/114208672145631483 a month ago. Sounds almost too good to be true, but I haven't tried it myself

Not too long ago, I went down a rabbit hole of specifying GHA yaml via dhall, and quickly hit some problems; the specific thing I was starting with was the part I was frustrated with, which was the "expresssions" evaluation stuff.

However, I quickly ran into the whole "no recursive data structures in dhall" (at least, not how you would normally think about it), and of course, a standard representation of expressions is a recursively defined data type.

I do get why dhall did this, but it did mean that I quickly ran into super advanced stuff, and realized that I couldn't in good conscience use this as my team of mixed engineers would need to read/maintain it in the future, without any knowledge of how to do recursive definitions in dhall, and without the inclination to care either.

an intro to this: https://docs.dhall-lang.org/howtos/How-to-translate-recursiv...

An example in the standard lib is how it works with JSON itself: https://store.dhall-lang.org/Prelude-v23.1.0/JSON/Type.dhall...

basically, to do recursive definitions, you have to lambda encode your data types, work with them like that, and then finally "reify" them with, like, a concrete list type at the end, which means that all those lambdas evaluate away and you're just left with list data. This is neat and intresting and worthy of learning, but would be wildly overly-complicated for most eng teams I think.

After hitting this point in the search, I decided to go another route: https://github.com/rhysd/actionlint

and this project solved my needs such that I couldn't justify spending more time on it any longer.


To me, the issue comes when something weird is going on in CI that isn't happening locally, and you're stuck debugging it with that typical insanity.

Yeah, it may be that you'll get the exact same versions of things installed, but that doesn't help when some other weird thing is going on.

If you haven't experienced this, well, keep doing what you're doing if you want, but just file this reflection away for if/when you do hit this issue.


I've experienced this, but not often enough that it feels like a significant barrier.


Ditto! I've never heard of "Standard Oil of New Jersey" myself.


Thank you to whoever changed the title to omit the question mark. Question marks at the end of non questions drives me crazy. I’ve tried to accept it and accustom myself to it, but I still always trying to parse it multiple times


That was me. I'm the idiot who originally added it to the published headline before I realized it and removed it.


Any statement can be a question with the right tone. I actually have the exact opposite stance: we expect formal questions when in most cases it makes more sense to simply state something with a lilting tone. But maybe it's just me?


Not all statements are headlines.


Nice. I recently have been doing a bash deep dive and realizing that treating it like repl driven development is very useful. Been wondering if this is worth writing about for others.

You can also just execute another ‘bash’ process, of course the environment is a bit different


I, too, recoil at displays of arrogance, needing to spend time communing with nature from a barrel.

Tho I haven’t started on my exploratory instrument crafting era.


reminds me a little bit of the Kernel programming language


Reminds me a lot, except that Kernel does away with "special forms".

Link for "Kernel" because it's a lousy name to have to search for: https://web.cs.wpi.edu/~jshutt/kernel.html

One interesting bit that remains unappreciated about Kernel is that environments are reified and are "copy-on-write". So, if you make an environment change, it propagates forward but previous calls only see the previous environment unless explicitly give them access.

It's a little unusual, but it works out pretty well. You can call the "ground environment" which is immutable and lets you compile things. You can access the "lexical environment" which works like normal. And you can call a "dynamic environment" that lets you capture things.

It's interesting in that it contains the scope of "dynamic environments" and still allows things to be compiled (which was the big downside of dynamic environments circa 1970s).

One downside I found is that the obvious implementation thrashes the hell out of your garbage collector. "Environments" really want a data structure that is more complicated and copy-friendly than "cons pairs".


> One interesting bit that remains unappreciated about Kernel is that environments are reified and are "copy-on-write". So, if you make an environment change, it propagates forward but previous calls only see the previous environment unless explicitly give them access.

That's not quite how it works. Environments are mutable, but only the locals of an environment that we have a direct reference to. We cannot mutate any of the bindings in the parents without a reference to the parents, and the language provides no facility to obtain references to the parents given only a reference to the child.

We can however, explicitly pass around references to parent environments if we do have access to them, because environments are first-class, and we can move around references where we want. We can have a child mutate its grandparent environment, and those changes will be visible to the child's parent - but this is only possible if the child has a direct reference to the grandparent - which it would not have under normal circumstances - it has to be provided with that reference explicitly.

We could for example, bind our top-level scope into a symbol in the top-level, such that any descendants of the top level can mutate any bindings in the top level.

    ($define! *globals* (get-current-environment))
    
    ($define! x 0)
    
    ($define! print-x
        ($lambda () (write x)))

    ($define! foo
        ($lambda ()
            ($set! *globals* x 1)))

    (foo)
    (print-x)
Here we see that the lambda defined for `print-x` does not get an immutable copy of the top-level scope, because if that were the case, then the `x` printed by `print-x` would be a different `x` that foo mutates, and `print-x` would still print 0, but this should print 1.

Obviously, we would not do this in practice because it would let us rebind `print-x` and `foo`, or even rebind `*globals*` to some another value.

A safer way to do this would be to bind a setter for x in the top level.

    ($define! x 0)
    
    ($define! $set-x!
        (($vau () top-level
            ($vau (value) #ignore
                ($set! top-level x value)))))
    
    ($define! print-x
        ($lambda () (write x)))
    
    ($define! foo
        ($lambda ()
            ($set-x! 1)))
            
    (foo)
    (print-x)
This would allow any descendants of the top-level to mutate `x`, but not mutate any other binding in the top level.

This works because the outermost `$vau` on $set-x!` is evaluated in the top level, where `$set-x!` is defined, so it receives the top level environment as its dynamic environment, which it binds to the symbol `top-level` in its local environment, and then returns an operative whose static environment is its own local environment.

When `$set-x!` is called, it ignores its caller's dynamic environment, but its static environment contains the symbol `top-level`, bound to the top level environment.

---

The immutable part of environments is the list of parents that any environment has. When we create an environment through say `(make-environment list-of-parents)`, the environment creates an immutable copy of the list of parents, which prevents whoever created the environment from being able to modify this list later. For example, we wouldn't want this

    ($define! parents (list some-environment))
    ($define! env (make-environment parents))
    ($set-car! parents some-other-environment)
The caller mutates `parents` after creating `env`, but this will not affect `env`, because it created an immutable copy of `parents`.

However, the creator of this list can mutate the bindings in the parent after creating the env.

    ($define! parents (list some-environment))
    ($define! env (make-environment parents))
    ($set! some-environment some-binding some-value)
This would be reflected in `env`, whose `some-binding` would now resolve to `some-value`.

But usually it will be the case that `some-environment` is encapsulated by the creator who will eventually return `env`, but no reference to `some-environment` - which will prevent any mutation of the bindings in the parents.

---

There is no way to mutate the ground environment because every standard-environment is a child of the ground, but with no direct reference to the ground. The ground is just its parent. The definition of `make-kernel-standard-environment` is interesting, because it's just

    ($define! make-kernel-standard-environment
        ($lambda () (get-current-environment)))
Because Kernel specifies that ground bindings are evaluated in the ground environment, and the lambda creates a child of the ground with no initial local bindings, then when we get that environment, we have an empty environment with ground as its parent.

Although Kernel is specified using mutable environments, the Kernel report does make them optional. It doesn't really go into depth about how one would implement Kernel with immutable environments besides a brief note.

If you do have immutable environments, then a fun challenge is how you would implement `make-kernel-standard-environment`. If using the definition above, then the environment returned by the call to `make-kernel-standard-environment` notably does not have the binding `make-kernel-standard-environment` in it (because the binding happens after).


How did you resolve all this? When I implemented Kernel, this was one of my big sticking points as to whether environments were only copy-on-write or genuinely mutable.

Section 4.3.1 and Fig 4.1 from his thesis doesn't really help clear this up much.

> Although Kernel is specified using mutable environments, the Kernel report does make them optional. It doesn't really go into depth about how one would implement Kernel with immutable environments besides a brief note.

Thanks. I must have missed that. I'll go look again at some point.

Part of the problem I had was that the environment handling was cons-ing up a lot of garbage. I really needed some way to default everything to "ground environment" to prevent the creation of a new environments all over the place.


> How did you resolve all this? When I implemented Kernel, this was one of my big sticking points as to whether environments were only copy-on-write or genuinely mutable.

I must have read the Kernel report several dozen times, implemented it a few times (both with and without mutation), and tested against other implementations - most often klisp, because it's the most complete.

There is some wording in it which makes you question whether bindings in parents can be mutated, but it's not explicitly stated that this should not be the case. There are suggestions that it should be.

We know that ground or any of it's ancestors are not meant to be mutated, as per Section 3.2

> Implementations are expressly forbidden to provide any means by which a Kernel program could capture or attempt to mutate any improper ancestor of the ground environment.

Most of the details about environments are in Section 4.8:

> Changing the set of bindings of an environment, or setting the referent of the reference in a binding, is a mutation of the environment. (Changing the parent list, or a referent in the list, would be a mutation of the environment too, but there is no facility provided to do it.)

As mentioned in my previous reply, the list of parents (and the references in that list) are the immutable parts. This doesn't say we can't mutate bindings in the parent - only that we can't mutate the ancestry.

> The Kernel data type environment is encapsulated. Among other things, there is no facility provided for enumerating all the variables exhibited by an environment, and no facility for identifying the parents of an environment.

This confirms that there's no way of capturing references to parents given we only have a reference to a child.

> In particular, whenever a combiner is called, it has the opportunity, in principle, to mutate the dynamic environment in which it was called. This power is balanced by omitting any general facility for determining the parents of a given environment, and also omitting any other general facility for mutating the parents, or other ancestors, of a given environment.

This is one of the confusing parts - but it doesn't say we can't mutate bindings in ancestors - only that there's no general facility to do it. Presumably, Shutt is referring to having no facility to mutate bindings in the parents if we only have a reference to the child.

> The (second-class) Scheme operative set! allows Scheme code to modify any binding that is visible, regardless of whether or not the binding is local. Binding mutation in Kernel is more controlled, because the environment mutators can only affect capturable environments — $set! mutates an environment captured by the client, while equipowerful primitive $define! does the capturing itself. Encapsulation of the environment type allows ancestor environments to be visible without being capturable, making them effectively read-only.

Similar to the previous one, he is comparing Kernel's $set! to Scheme's set! to indicate that $set! can only mutate the locals of a captured environment. (Ie, that the parent bindings are effectively read-only, given our only reference is the encapsulated child environment).

The main hint that he intended it to be possible is in the rational of $provide!

> When more than one combiner is exported to the surrounding environment, it would be technically possible to locally capture the surrounding environment and use $set! for the exportation:

    ($let ((outside (get-current-environment)))
        ...
        ($set! outside (foo bar quux) (list foo bar quux)))
> but this would be bad style because the local binding of outside is visible to everything in the local block, granting power to mutate the surrounding environment to everything in the local environment, and therefore (probably) to anything that captures a descendent of the local environment.

---

> Thanks. I must have missed that. I'll go look again at some point.

All of the sections on mutability are marked (optional) in their headings!

In Section 4.9 he gives a brief note about the optionality, but doesn't dive deep on how to implement it.

> It isn’t clear to what extent one can do serious Kernel programming without mutating environments; but separating the mutators into an optional module allows language implementors to explore this question within the bounds of Kernel specified by this report. In the absence of environment mutators as such, the programmer would presumably fall back on Kernel’s rich vocabulary of environment constructors (notably the $let family), which the report does not class as mutators, although environment initialization is routinely described in terms of adding bindings.

> Per §3.4, language extensions are judged against features described in the report rather than against features actually supported by the extending implementation; so, failure to support features in the Environment mutation module as a whole does not prevent a non-comprehensive implementation from providing alternative means for doing some of the same things

My experience with implementing Kernel without mutation is that it's probably better to give up following the report to the word, but we can get something almost equivalent by taking some liberties. The main liberty I took was to modify the evaluator so that it returns an environment along with the value, and this environment is used for evaluation of the next expression. A $define operative (note the absence of !) creates a child of the current environment and binds its symbols into this new child, which becomes the new current environment. This can avoid the need for defining the entire ground in $letrec* and having the user-program be run in its body.

Special care needs to be taken for recursion when not having mutability, because the examples of recursion in the Kernel report have a subtle dependency on mutation of the environment - namely, they reference a binding which does not exist yet, but by the time the recursive function is defined and bound, it now exists in the static environment.

This itself is another hint that bindings in parents aught to be mutable - without this, recursion as used in the Kernel report would not work. Consider one of the trivial examples of recursion in the report.

    ($define! $or?
        ($vau x e
            ($cond ((null? x) #f)
            ((null? (cdr x)) (eval (car x) e))
            ((eval (car x) e) #t)
            (#t (apply (wrap $or?) (cdr x) e))))) ; recursive call
Here $vau captures ground as its static environment - but at the point this capture occurs, $or? does not exist in the ground environment. The $vau combination is first evaluated, and then the resulting operative is bound to $or?

If the environment were not mutable, when the recursive call to $or? is made, it would look up the immutable copy of ground it took as it's static environment, which does not contain $or?

But clearly, this recursion is intended to work, so $or? must be available in the static environment when the recursive call is made - so a mutation of the ground was made that the body of the $or? operative is witness to, even though it is in a child of ground.

This is related to my previous point about `make-kernel-standard-environment`. Without mutability, it's bizarre, because it exports a standard environment without the `make-kernel-standard-environment` binding in it yet. To do this without mutating the environment, we could try to define it twice:

    ($letrec* ((...all other ground bindings...)
               (make-kernel-standard-environment ($lambda () (get-current-environment))
               (make-kernel-standard-environment ($lambda () (get-current-environment)))
But even this is problematic. When we first call to `make-kernel-standard-environment` in the body of this $letrec*, it returns an environment which contains `make-kernel-standard-environment` - but if we call `make-kernel-standard-environment` on that environment, it returns an environment which doesn't contain it.

A proper solution needs to use a combination of $vau, $letrec, $bindings->environment and make-environment.

    ($letrec* ((...all other ground bindings...)
               (make-kernel-standard-environment  ;; must be the final binding in ground.
                    ((wrap 
                        ($vau () e
                            ($letrec ((make-kernel-standard-environment
                                        ($lambda ()
                                            (make-environment
                                                ($bindings->environment
                                                    (make-kernel-standard-environment 
                                                     make-kernel-standard-environment))
                                                e))))
                                    (get-current-environment))))))))
It could be done also using $let-redirect instead of $bindings->environment.

$letrec* itself probably needs to be primitive without mutation. I'm not sure how you could implement it without $define! or without already having a $letrec equivalent. Maybe some clever trick with $vau.

---

> Part of the problem I had was that the environment handling was cons-ing up a lot of garbage. I really needed some way to default everything to "ground environment" to prevent the creation of a new environments all over the place.

I don't think you can really prevent the creation of new environments in Kernel. It's pretty explicit in the definition of $vau that a new environment is created to hold the locals, and that its parent is the static environment where the $vau expression was called.

Perhaps one thing that could improve the way ground bindings are accessed would be to generalize the idea behind the `keyed-static-variable` feature, but apply it to all ground bindings too. Basically, have a kind of "key environment" type, whose bindings cannot be shadowed in child environments, and have the ground environment be one such key environment. This approach could speed up symbol lookup for ground symbols, because currently they're at the bottom of a depth-first search. If they were not shadowable, they could be searched before non-key symbols. However, this is diverging a fair bit from the Kernel spec.


Thanks for taking the time to write up such a detailed response.


Either human blood or potatoes, the two options.


Don’t forget “do while”, and the various control statements they support.


And continue and break


Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: