Hacker News new | past | comments | ask | show | jobs | submit login
Kamby – A programming language based on Lisp that doesn't seem like Lisp (kamby.org)
155 points by henriquegogo on Sept 8, 2022 | hide | past | favorite | 52 comments



I think it's great that people create new languages. It's fun and a good learning exercise. For this language I'm very unclear on the scoping rules, and the dynamic environments look quite hard to reason about. I can't imagine ever using the language, but I'm pleased the creator is exploring some off beat ideas.


> Variables are not unique. You can declare multiple values for the same variable name using ":=" and append in the stack. If you want to edit the last declaration, just user simple "=" operator. To remove the last variable with specific name in stack, use "del varname"

Isn't this very confusing in almost all instances?


Forth behaves the same way. The vocabulary is really a stack, and you can define words, redefine them, and then pop them back off to return to the old value.

It's really not useful for variables, but can be very useful for functions, and since they're pretty much the same thing, you get the behavior for free. Although I haven't touched Lisp, my understanding is that Forth and Lisp have some conceptual similarities, but Forth is written by an engineer and Lisp by a computer scientist. Limit you for your own safety? Not a thing in Forth.


Seems like an explicit/less confusing way to do variable shadowing, though imho that's not a feature worth implementing in a 400LOC language.


It being explicit/arbitrary makes it more confusing, not less. At least most languages with variable shadowing, there's a block of some sort that defines the scope. Here it'd just be anywhere that it can change meaning.


In block scoped languages, you cannot access variables outside of the scope. They disappear entirely when the scope terminates (exemplified by C and its ilk), or are captured in a closure (Lisp) possibly downward-funarg-only (Pascal) such that only the body of the closure can access them.

In this Kamby language, it looks like the bindings survive and are then accessible by name. When you're executing these [ ... ] blocks, a tree-shaped environment is built up which is then navigated with the :: operator, enabling it to simulate objects with fields.

The idea of there being a stack-like environment of variables which can survive the blocks in which they are bound is very useful in pattern matching and unification.

Under pattern matching, the successful path will have the cumulative variables arising from everything that has successfully matched, including some nested constructs. However, the search strategy will implicitly backtrack in searching for matches, and in backtracking, it will implicitly erase any variables accumulated in the discarded paths.

In the TXR pattern language, I invented some mechanisms for controlling the proliferation of variables. When a pattern function is used, only those variables that correspond to its arguments can emerge.

  $ txr -B -c '@(define fun (arg1 arg2))
  @(bind arg1 "a1")
  @(bind arg2 "a2")
  @(bind arg3 "a3")
  @(end)
  @(fun x y)'
Output:

  y="a2"
  x="a1"
How it works is that the unbound variables x and y are identified with the arg1 and arg2 variables. The pattern function executes, binding arg1, arg2 and arg3. Then a resolution step is done at return-time. Because arg1 was identified with unbound x, x now receives that binding. Similar for y. The variable arg3 disappears; that entire local environment of the function is discarded, and x and y are grafted onto the original environment that existed on entry into the function.

If we bind an argument, the parameter will be bound on entry into the function, and so then has to unify in the subsequent bind directive. It does if we pass the value "a1":

  txr -B -c '@(define fun (arg1 arg2))
  @(bind arg1 "a1")
  @(bind arg2 "a2")
  @(bind arg3 "a3")
  @(end)
  @(fun "a1" y)'
  y="a2"
fails if we pass some other value:

  txr -B -c '@(define fun (arg1 arg2))
  @(bind arg1 "a1")
  @(bind arg2 "a2")
  @(bind arg3 "a3")
  @(end)
  @(fun "foo" y)'
  false
The variable binding environment isn't reified as a value that can itself be bound as a variable and inspected; in any context, there is one environment which contains everything that was done in the chain of pattern matching leading up to that point; dead ends that were backtracked out of are gone.


> less confusing way to do variable shadowing

How is this less confusing? := vs = will either push a new variable on top of a stack of identically named variables, or change the content of the top element on that stack? Why would anyone want any of that? Shadow variables are usually associated with hard-to-find-bugs, (i.e. forgetting that there is already a variable named 'xyz' in the current scope) not some great feature we want more of..


FWIW I think the author is trying to simulate let binding (or lambda binding, or both). But since there’s no syntactic scope you have to do your own stack management. Ugly and clunky. But this isn’t really a language intended to be used. It’s sort of like lisp assembly.


So this language is based on dynamic scope? I'm not sure about confusing, but why wouldn't I want to just use LOGO then?


What current LOGO implementation would you recommend?


NetLogo


What is Kamby? A small, embeddable and convenient language for who want to use and understand what is happening behind the scenes. The core is just ~400LOC and binary has just 20kb.


The more I explore the github, the more amazed I am... the same code compiles to WASM too? Thanks!


yeah! And you can test an run it in official website.


Hi! This looks super cool. I'm curious at what inspired you to implement variable stacking. At first I was like "whaaaaat", but then I thought, "maybe everything being a stack is useful?" Maybe for like immutable state or something.

Anyway again very inspiring :)


This should probably be marked a Show HN.


By the way, I always wondered why not just take a lisp, put every single thing (even an operator) on a separate line and indent every level of parentheses with a space or two (4 spaces is too much - deep nesting would go too much to the right). So there would be no need for the parentheses in most cases.


This is a divisive question, because of survivorship bias. Remember when the WWII Brits wanted to reinforce planes where they saw bullet holes coming back, till a statistician asked why no planes came back with holes in the other spots?

People who use lisp like the parentheses.

I don't, though they're no more objectionable than all the {}; languages. I just don't like unnecessary punctuation.

I coded in scheme for years using a preprocessor that understood

    define-syntax opt
      syntax-rules |
        $ _ (x v) b ...
          let || x | if (null? x) v | car x
            b ...
which is line for line equivalent to

    (define-syntax opt
      (syntax-rules ()
        ((_ (x v) b ...)
          (let ((x (if (null? x) v (car x))))
            b ...) )))
It's a matter of taste, but I'd rather read the former. The key ideas, other than using indentation to carry parenthesis level, are to use $ to hang double indents, and | to open a parenthesis that auto-closes.

I gave this up learning Clojure so I could use other people's tools. Instead I prefer lighter parentheses, and I use this script to tailor fonts for coding lisp:

https://gist.github.com/Syzygies/226253bc38743ef474ee67cbf58...

I have the most trouble with comment characters, in any language. In various languages I used a preprocessor that implemented a practical version of "comments are flush, code is indented" (hey, it cost me one level) and relied on syntax coloring to mute the comments. Again, I gave this up to use other people's tools.


I think sweet-expressions[0] address this, though they haven't caught on much.

[0]: https://dwheeler.com/readable/readable-s-expressions.html


If you learn Lisp, both the language and the tooling, you'll come to really like the parens. I suggest to everyone who uses Lisp, just set your editor's syntax highlighter to make the parentheses fade into the background color almost completely. They're still there, for correctness, structured editing, and ease of sharing with others; but they don't stand out.


A technical reason not to do that is that if you have the parentheses and the indentation then you have the structure encoded in two ways. This is useful; if the two differ, that indicates a problem.

Another technical reason is that we work with text based version control tools, under which we use whitespace-insensitive diff as a hack to hide some differences that don't make a difference. That tool becomes unreliable over indentation-only languages; you can make a semantic change whose whitespace-insensitive diff is empty.

Another technical reason not to go indentation-only is that you don't know whether a partially obscured block of code is complete or just a prefix:

   |defun foo():
   |   bar()
   |   xyzzy()
   +---------- < window border
am I looking at all of foo, or do I have to scroll down to see more?

Here you know it's the whole thing:

   |(defun foo():
   |  (bar)
   |  (xyzzy))
   +---------- < window border


What would this accomplish aside from spreading your code all over the screen, making it both hard to read and edit, and reducing your freedom of formatting for semantic sense?



Then programs will be too long. Too much scrolling, less information density on screen, more difficult to comprehend.


Did someone just reinvent Rebol/Red?


I wish someone would reinvent Rebol/Red with understandable scoping rules. One of the few reasons I don't use the language is because the scoping/binding rules are just completely inscrutable.


Exactly this. Thank you.


It's similar in some parts, but I would say that I inspired more in TCL


Browsing the source code, I came across something completely unexpected. I didn't know you could put code in structs like this sample from kamby/kamby.c

  struct KaNode *ka_add(struct KaNode *node, struct KaNode **env) {
    if (node->type == KA_STR && node->next->type == KA_STR) {
      struct KaNode *output = ka_str(node->str);
      strcat(output->str, node->next->str);
      return output;
    }
    return ka_num(node->num + node->next->num);
  }
Is this legal? If so, I'm going to use the heck out of it for my version of STOIC.

[Edit] Ok.. thanks for the help.. I'm used to seeing function types declared AFTER the parameters because pascal habits die hard. It looked like a run of the mill structure declaration to me.


I don't do a lot of C, but isn't this just a global function returning a pointer to a KaNode? If I remember correctly this is just Cs way of specifying a type is a struct same thing with the line in your example "struct KaNode *output = ..." someone feel free to correct me if I am wrong though.


Would be easier to read if KaNode was a typedef


That’s a function that returns a struct. In C structs live in their own namespace and references to them have to use the struct keyword unless you define a typedef.


That's not a struct. It's a function called ka_add that returns a pointer to a KaNode


If it makes you feel any better, I can't remember the last time I saw that construction in the wild. Idiomatic C codebases use a typedef for this 10 times out of 10.


I don't really know C, but I think this is a function returning a "struct KaNode*" type, not a struct.


Lol, that's a function that returns a pointer to a struct.


So, it's like Hy, only the other way around?


I can feel that this is surely a fun project, full of decent tricks on the code level. Still, tricks are just tricks, and won't provide firm foundation. It's gonna be difficult to engineer either an application or the language itself in the current state. I kinda wonder what OP is planning after this.


Ummmm

planet = [ name := 'World' nick := 'Earth' ]

'Hello, ' + (planet :: {WAT})

Output: 10585168


Obviously.


If I visit the website, Firefox warns of potential security risk because the connection is not encrypted.


I made a small evaluator in TXR Lisp for what appears to be my understanding "Kamby-like" semantics.

  $ txr -i kamby.tl 
  Syntactic toffee recipe: melt butter over low heat, stir in Lisp macros.
  1> (kamby-repl)
  kmb> (let dz (/ 10 0))
  ** error: /: division by zero
  kmb> (let a 1)
  a
  kmb> a
  1
  kmb> (let foo [(let bar1 42) (let bar2 73)])
  foo
  kmb> foo
  #<kamby-env:(bar2 bar1)>
  kmb> foo.bar1
  42
  kmb> foo.bar2
  73
  kmb> (set a 4)
  4
  kmb> a
  4
  kmb> (set a (* 2 a))
  ** warning: unbound variable a
  ** error: unbound variable a
Errors out at the end because unknown forms are evaled as Lisp, and Kamby variables are not Lisp variables.

Mostly this was to play with the "construct environment tree as-you-go, with qualified pathname access" idea.

Code:

  (defstruct kamby-env ()
    bindings      ;; assoc list
    next-env      ;; kamby-env object or nil

    (:method extend (me sym value)
      (upd me.bindings (acons sym value)))

    (:method lookup (me sym)
      (assoc sym me.bindings))

    (:method delete (me sym)
      (let ((binding (assoc sym me.bindings)))
        (upd me.bindings (remq binding))))

    (:method print (me stream pretty-p)
      (format stream "#<~s:~s>" 'kamby-env [mapcar car me.bindings])))

  (defvar *kamby-env* (new kamby-env))

  (defun kamby-error (form fmt . args)
    (error `~s: ~s: @fmt` 'kamby-eval (if (atom form) form (car form)) . args))

  (defun kamby-eval (form)
    (let ((e *kamby-env*))
      (match-case form
        ((let @var @expr) e.(extend var (kamby-eval expr))
                          var)
        ((set @var @expr) (let ((binding e.(lookup var)))
                            (if binding
                              (rplacd binding (kamby-eval expr))
                              (kamby-error form "no such variable: ~s" var))
                            (cdr binding)))
        ((del @var) (unless e.(delete var)
                    (kamby-error form "no such variable")))
        (@(symbolp @var) (let ((binding e.(lookup var)))
                            (if binding
                              (cdr binding)
                              (kamby-error form "no such variable: ~s" var))))
        ;; TXR Lisp qref syntax   a.b.c.d  <--> (qref a b c d)
        ((qref . @vars) (let (value)
                          (while vars
                            (let* ((var (pop vars))
                                   (binding e.(lookup var)))
                              (unless binding
                                (kamby-error form "no such variable: ~s (when resolving ~s)" var form))
                              (set value (cdr binding))
                              (if (and vars (not (typep value 'kamby-env)))
                                (kamby-error form "~s isn't an environment object; cannot lookup ~s"
                                             value (car vars)))
                              (set e value)))
                          value))
        ;; TXR Lisp dwim brackets syntax   [a b c]  <--> (dwim a b c)
        ((dwim . @forms) (let ((*kamby-env* (new kamby-env next-env e)))
                           [mapdo kamby-eval forms]
                           *kamby-env*))
        (@else (eval else)))))

  (defun kamby-repl ()
    (whilet ((line (progn (put-string "kmb> ") (get-line))))
      (catch
        (prinl (kamby-eval (read line)))
        (error (x)
          (put-line `** error: @x`)))))


Where are the macros?


It’s not actually s-expressions but the front page gives you a pretty clear reader algorithm. The macros are sitting right there waiting to be brought.


Yeah, I think the title should be updated to something less elaborative. Looks like the reference to Lisp comes from the Github Readme?

https://github.com/henriquegogo/kamby

Still looks awesome; 400LOC is an incredible achievement for grok-ability c:


And yet its still filled with unncessary parentheses like an involuntary tic.


Lisp code tends to have fewer delimiting punctuation characters than most algol style current languages. I think people tend to dislike languages that have only the minimum necessary, because it's easy to misread code then and redundancy helps compilers catch mistakes as inconsistent syntax.

But there's always Forth for the the lighter taste in delimiters: https://github.com/TexTerry/forth-examples/blob/master/pasca...


I never understand these complaints about parenthesis, my editor takes care of them. Interestingly people never complain about the inconsequences in mathematical notation, i.e. infix operators like plus or minus (e.g. 3+5), postfix operators like faculty (i.e. 9!) and last but not least prefixed user defined funtions like f(x).

But as Lisp was my first real language, I might be a bit biased. And besides Lisp I like languages like Forth or Postscript, and even Perl ;-)


What editor takes care of your parentheses? I haven't found anything quite painless enough for me to want to dive into all that punctuation. Are you talking about paredit? Parinfer? Something else?


It’s not about writing but about readability. Clojure is easier to read than Scheme because it uses more punctuation (brackets and braces instead of only parens.

There’s a point in using different signs that mean different things.


That's great, but they specifically sold it as not having Lisp-like syntax. And yet its incredibly Lisp-like.


At a glance it appears to have less punctuation than Python. Slightly more than YAML. Do you want ambiguity? This is how you get ambiguity.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

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

Search: