The Janet Language
janet-lang.org
janet-lang.org
Parsing expression grammars (think, like, declarative parser combinators?) are a really great feature for ad-hoc text parsing -- and nicer than regular expressions, in my opinion, for anything longer than about ten characters.
The language itself is also a great introductory language-with-lots-of-parentheses, so you can explore compile-time metaprogramming with familiar runtime semantics -- think JavaScript plus value types minus all the wats. The embeddability and easy distribution (compiles to a static binary on any platform) is a huge plus as well.
Honestly I like Janet so much that I'm writing a book about it, because I think it's a shame that the language isn't more well-known. It's great! You should check it out right now!
> I feel like if I used it, it would atrophy my skills in other more traditional languages.
was not the case for me at all. If you go into a text editor and remove all the parentheses, I find that's how Lisp programmers tend to see Lisp, (function argument) isn't that far from function(argument).
Learning Lisp has only improved my skills as a programmer, after getting ideas like code as data, macros, let over lambda, CLOS and the metaobject protocol. It's a simple model that to me shows how other languages have picked an abstraction and stuck with it, but Lisp has all the tools to implement those abstractions and more.
More mainstream languages are great at focusing the developer, and that makes them very practical. It is amusing though to watch many of the "new features" in languages come out even though Lisp had them years ago.
The simplicity and symmetry of the syntax is a big part of that love for me. Being able to manipulate lisp code as lisp data, using the full power of the language to do so, is just brilliant.
Janet looks lovely! Looking forward to the book.
The opening paren is simply relocated to the other side of the function name or keyword.
What we're experiencing is just a cognitive bias which causes us to prefer the more familiar over the less familiar ([the mere-exposure effect, also called the familiarity principle](https://en.wikipedia.org/wiki/Mere-exposure_effect)).
I've never used a Lisp-family language, but I find the reaction against parentheses to be overblown.
The reading order of the code is also _consistent,_ rather than the frequent switching between infix notation, prefix notation, and postfix notation which we have to learn and parse in most languages outside the Lisp family. This is a benefit which deceptively looks like it is _more_ complicated, despite being simpler to parse visually (and otherwise). Another example of the familiarity principle at work.
This tired trope needs to die. Yes, there are more parentheses, because lisp also uses parentheses where other languages use [] or {} or ;.
The real thing is that people from C-like languages are used to seeing different block markers for different constructs. It takes effort to read Lisp coming from other languages, because those other languages have a richer symbol vocabulary. Learning to read code without those symbols is like reading English where all punctuation has been replaced with a single space. Sure you ll get there eventually but it s a very cheap straw man argument to pretend that the only complaint people have about Lisps is the positioning of the parentheses.
I recently started using Clojure and I’ve used languages like C#, JavaScript, and Python a lot. My two cents is that a Clojure-like language should try to embrace the aesthetics of a white-space language like Python, but use the parens as clues for scopes or blocks. So much could be done with formatting rules that just make parens easier to scan without some extra IDE highlighting or something.
The best part of parens is that you can try to pick a consistent format, but ya know that sometimes doesn’t happen because everybody likes to use parens differently lol.
Lisps don't arbitrarily look weird—there's a deep, principled, elegant reason for it; Lisp code represents how the code will be evaluated in the most direct way, without relying on (some would say needlessly) complex parsing / precedence rules. There are no surprises and no arbitrary rules to learn. There are no useless semicolons to forget, and you'll never have to wonder if `+=` returns the RHS or the result of the operation (or does it even have a return value?).
You don't have this meaningless distinction where you can't directly reduce with `+` because—ugh—it's not a function, it's an operator. You just say `(reduce + [1 2 3])`.
You never have to do this ugly Ruby stuff...
words.map &:length
# or
words.map { |w| w.length }
...because methods are really just polymorphic functions, but language designers chose syntax that doesn't compose elegantly.You don't have this useless distinction between statements and expressions that limits how you can compose code. You never have to drop down to some ugly, limited tertiary expression form (`COND ? X : Y`) of `if` because—whoops—`if` is a "statement". You just write:
(println (if me? "me" "you"))
Because, duh, we wanted an `if`.What do we gain by adding all of this noise?:
if (is_me) {
println("me");
} else {
println("you");
}
Absolutely nothing. The parens on the conditional, the curly braces, the semicolons, the `else` keyword—they're essentially meaningless incantations to appease the compiler. And we've introduced an undesirable opportunity for the two branches to accidentally diverge over time.But most importantly, our code is written in the data structures of our language. Code as data means we can manipulate code as easily as we manipulate data, which means we can trivially (re-)write code with code (i.e. macros). And not shitty string generating macros, or macros that can only do a handful of sanctioned things—we can write our own control structures in couple lines of code. We can add new abstractions to the programming language from user space.
Wish the language had an `if-not` construct? You can add it with, like, 3 lines of code. Wish functions could have optional parameters? Add it. Wish it had a pattern matching functions like SML or Erlang? Cool. Java-style annotations? Logging that is fully removed when running in high performance mode? A different OO model? Multi-line string literals? String interpolation? A graph of dependent calculations that only get run when used? A more convenient way to load dependencies? It's all easily doable.
I've coded in Lisps (and a dozen other languages) for at least 20 years, and every time I have to use a non-Lisp syntax I just think "wow, these people really missed the boat". It's like having to write math in Roman numerals (would you rather calculate "D + L + IX" or "500 + 50 + 9"?); there's a better way, and that better way has elegant, recursive, underlying design principles that make the ergonomics way better.
But, yeah, it doesn't look like C code. And people seem to be really attached to their C syntax.
println(is_me ? "me" : "you")
which has exactly the same amount of symbols.Of course you can do that simple case with a tertiary operator but:
1. It's a construct that really has no reason to exist (I argue) as distinct from `if`.
2. It doesn't compose with statements.
This duality is primarily what I'm arguing against.
A better example would have been a case statement inside of the `println`:
(println
"Log in by"
(case user-id
0 "root"
1 "local admin"
(format "regular user (id: %d)" user-id)))
In C, you have to introduce a variable for no good reason (or do some non-idiomatic, ugly, nested tertiary operators that get uglier the more cases we have).And even then, you can't just say
user_name = switch { ... }
Because switch is an statement.For example, the sum of a list (not running or cumulative sum, but total sum. Should equal 10, not 1, 3, 6, 10):
Lisp:
(+ 1 2 3 4)
J/APL[0]: +/1 2 3 4
Python: Sum = sum([1,2,3,4])
print(Sum)
They all do the same. I prefer the conciseness of J/APL and Lisp in this case, and the application of a function over the list or vector. The beauty of the REPL is that you see the result without a 'print' statement.Solving problems in these other languages will influence how you program in your base language as well, and usually for the better. I am also guilty of syntax bias. I prefer LFE (Lisp Flavoured Erlang)[3] over Elixir and Gleam. Gleam[1], another language that runs on the Erlang VM (BEAM), had a more ML syntax, but then it chose to join the syntax popularity contest and move to a more Algol/C/Rust syntax. I prefer vanilla Erlang over it too.
[0] https://www.jsoftware.com/#/ [1] https://gleam.run/ [3] https://lfe.io/
If your skills in other languages are tied to a syntax then you never had any skills to begin with. I've used pretty much every syntax (and too many languages) out there and the only difference I've ever found is that ML syntax is nicer for automatically curried functions and LISP syntax is much nicer for meta-programming. The rest is effectively all down to paradigms, runtimes and libraries.
This is why I don't do any math I can't do on my fingers.
Parentheses are just too scary, and there's no way that parenthesis math junk actually has any useful ideas.
Lispers don't feel positive about Lisp because of parentheses; change them to curly braces or brackets or ^ and $—that's really not what matters. Lisps with brackets go all the way back to the beginning (https://en.wikipedia.org/wiki/M-expression). Indentation-based Lisps have been done too (https://readable.sourceforge.io/).
The point is an expression-based syntax that directly models the code tree, is written in the data structures of language, and is convenient for meta-programming. It's a fundamentally different approach that yields massive benefits (see my other comment in the thread if you want to hear that spelled out in more detail).
But we don't see that when we just stop at unfamiliar syntax.
Lispers have been structurally-editing code as a matter of course since at least 1970. Most of the rest of the world only got a taste of that when tree-sitter came out circa 2018 (I know I'm rounding the edges here, but the point stands). Half a century later! Why is that? It's not just curly braces vs parens—something deeper is happening here.
I do apologize if I came off rude. I'm just so frustrated at hearing this same line year after year after year from people who are missing out some of the most powerful ideas in programming because they prefer this ASCII glyph over that one. It's nothing more than parochialism.
It just makes me want to scream (perhaps uncharitably) "surely you're not a serious engineer who works on serious problems if your biggest concern while coding is which character is used to group code?!" I want tools to help me think more clearly, ways to operate at higher levels of abstraction, better concurrency semantics—surface characteristics be damned. Sure, I have my preferences about orthography, but the tail doesn't wag the dog.
Look deeper! Learn what each language has to teach you! Then keep the parts that move our craft forward and use whatever glyphs you want. But don't reject the automobile because it doesn't have handlebars.
Moreover, the things that look familiar probably have the least to teach you.
I believe we have the ability to do so much better as an industry, but it's not going to happen if we reject the unfamiliar just for being so.
Though arguably lispy, Janet just wasn't lispy enough for me. It was missing the simple, elegant sexp syntax I dearly loved, and I started to wonder what huge win I was getting from using it instead of just using Lisp or Scheme? Having not found a good answer, I did just that.
That was the last time I bothered trying to learn a "Lisp-like" language that wasn't actually a Lisp, and decided to just stick to Lisp/Scheme. They do everything I need, are good at math, and are plenty fast enough for me.
Those extra bits of syntax that makes it "not a lisp" are mostly around defining "not list" kind if data structures. I find it practical.
- array: not a singly-linked list.
- hash table: often an array of singly-linked lists, so not a list.
- red-black or AVL tree: can be built with cons cells.
- doubly-linked list: not a singly-linked list
- double-ended queue: array of double-ended queues, so not a list. Could also be implemented as a doubly-linked list.
I understand your point if you are speaking in literal terms about just simple cons cells, but in practical Lisp/Scheme code, you don’t really rely on just the basics to do things.
I think there’s a hyperfocus sometimes on the simplicity of the core of lisp, the apply/eval balance, but it’s quite possible and often easy and convenient to perform normal programming tasks with these languages as well.
Agreed.
> So what about streams? Functions? Closures? Call/cc structures?
Those are interesting examples. They are all data structures in a sense (especially streams and closures), but to me they are more like functions than data (yes, yes, functions are values, blah, blah). Call/cc is a reification of execution control; thinking of it in terms of data stretches my brain.
std::map is not a good example anyway, you want to consider std::unordered_map for a more appropriate comparison. C++ is weird that way. (What C++ calls a map is not what most languages call a map. std::map doesn’t even satisfy O(1). You’d be surprised how many working C++ developers don’t even realize the unnecessary performance cost they take when they decide to use std::map, because it’s not a proper hash map. )
But this thread is not about the finer differences in implementation of maps but rather whether or not they are basically just lists. They are not.
Please measure before you make changes to your maps for perf reasons. Yeah this forum all knows their big-O, but B-tree maps like std::map often perform better than hash maps on real-world architectures.
std::set and std::map should be std::ordered_set and std::ordered_map sts::unordered_set and std::unordered_map should be std::hash_set and std::hash_map
If this were so then it might make the incorrect usage of these two options less prevalent. But they are both map and set structures just each version has other guarantees that in some scenarios may be more or less useful.
And similarly, linked lists and arrays are just different "versions” of the same data structure: a list.
Imagine someone says, every fruit in the world is sour, and someone answers, no there are plenty of fruits that are sweet, such as apples, and then an entire thread gets launched in an irrelevant direction pointing out that actually some apples are sour, which has no bearing on the original point.
You could fill that with linked list nodes, but it would be pointless.
Maybe in 0.01% of the cases. In reality they just ruin your cache and memory allocator performance for no real good reason.
Hell, memory allocators themselves are often implemented using some form of linked list. You tend to see them quite a bit at very low levels like in kernels.
What Janet makes a 'not really a Lisp' is that "LISP" stands for "List Processor". Janet isn't exactly that, since it is not using linked lists as a core data structure - unlike Lisp where its List Processing features are built on top of linked lists made of cons cells.
(1 2 3) is called a "tuple" in Janet and represents something like an immutable array.
CL-USER 40 > (rest '(1 2 3))
(2 3)
Above REST operation does not allocate any memory. CL-USER 41 > (subseq '#(1 2 3) 1)
#(2 3)
Above SUBSEQ returns a new vector. Alternatively it would need a more clever implementation underneath.OTOH getting a random element has a different complexity in a linked list vs. a vector.
Or maybe your particular Scheme has all of that out of the box, too. Which one do you normally use, if you don't mind?
In what sense does Janet not have sexp syntax? Seems plenty sexpy to me. Purists seem to say it's not a lisp because its underlying data structure is not (cons-based) lists as in classical lisp, but I don't see what syntactic difference there is.
I wish I could take it back, but HN won't let me delete my post. I apologize. Mea culpa.
I like Lisp languages, and would take Scheme over Python for my job in a heartbeat, if I was allowed to. But, I think, that if we want interactive Shell-like programming, we really need to address the issues above in the language. Shell pipes are a good start, but they are quite restricted in what they can do and require xargs "extension". Some languages also have the "where" form, where arguments to a function can be elaborated after the function call.
If I was ever to design a language for interactive programming, I'd certainly try to have these two features in it.
That's one thing I will say after coming from Perl/PHP to Java, is that despite its verbosity and the uselessness of having to write .stream(), I much prefer Java's stream.map(...).filter(...) syntax over the more functional-style filter(map(list, ..), ..) syntax. The Java syntax reads left-to-right, which is the order you want when you're thinking about code, and also as you say writing it. I think if I were creating a programming language I too would try to make stuff read left-to-right as much as possible.
That's why I use and msybe tend to abuse the `->` and `->>` macros in Clojure and the pipe operator `|>` in Elixir.
Hopefully soon in JS as well, if I've read correctly.
@source >>> map { ... } >>> grep { ... } >>> my @sink;
though note I'm typing from memory on my second coffee so I may have got that slightly wrong.Plus of course there's many languages with a |> operator so you can do
g(x) |> f
I also (the example is specialised for I/O but the implementation technique could trivially be borrowed for something that wasn't) implemented something sort of in this direction for perl once: http://p3rl.org/IO::PipelineI'm not convinced that left-to-right is -always- the best option and prefer having the choice of both, but I wouldn't be at all surprised if a survey of developers found that if they could only pick one they'd pick left-to-right, and while I'd find it hard to choose for myself alone I'd probably pick left-to-right on the basis that it'd likely make it easier to onboard people to any given codebase.
my @sink = @source.map({...}).grep({...}).sort;
Which also makes multithreading the operation easy: my @sink = @source.hyper.map({...}).grep({...}).sort.list;
That said, I think the operator you're looking for is the feed operator, ==>: my @sink;
@source ==> map {...} ==> grep {...} ==> sort ==> @sink;
It also has the corresponding reverse, <==, because why not. The docs* mention that ==> may at some point do automatic parallelization, but I don't know what the status of that feature is.Yes, yes it was.
Your clarifications, corrections and elaborations are much appreciated.
Of course, Ruby is a bit of a mixed bag, but for the applications where it fits, it can be very nice.
In Common Lisp:
CL-USER 37 > (sin 3)
0.14112
CL-USER 38 > (cos *)
0.9900591
Above really is (cos (sin 3)). The variable * is bound to the last result.When used interactively, Python also has _ to store the previous value (but Python only ever really returns single value, which is sometimes a tuple or a list that can be deconstructed into variables, iirc in CL if you don't request other return values, they are gone.)
More generally, you'd want more of xargs-like functionality (eg. split result into chunks and feed them to the next function in chunks, perhaps in parallel). Or maybe you'd want something like a tee, to split the results of the previous function into multiple streams and processed by different functions? Java-like languages don't immediately support something like that, but Shell-like do with redirection syntax, tee, xargs.
But in Lisp you are not bound to the language syntax of Java. You can inside the language write tools to process forms. That's one of the main differences between Java and Lisp. Lisp has reader macros (to change the surface syntax of s-expressions) and macros to change the expression syntax. That would allow you to write tools to process code and results in arbitrary ways.
Multiple-values results in a REPL are handled by the variable /. That one is the list of the last values.
CL-USER 4 > (values 1 2 3)
1
2
3
CL-USER 5 > (multiple-value-bind (a b c) (values-list /) (list a b c))
(1 2 3)
But anyway, I would not write Common Lisp in a pure terminal without editing support.Something like GNU Emacs can use tools like SLIME or has a SHELL mode. That one works fine in a terminal.
SLIME is one of the Common Lisp IDE extensions for GNU Emacs and works fine in a terminal. Writing even the most complex code inside an GNU Emacs & SLIME terminal session is no problem at all. If I have a function call (foo a) and I want to wrap code to the front, I would just move the cursor one s-expression back and type. I'm sure that's easier with one of the other editor extensions which make editing s-expressions kind of structural.
EVEN then, many people write the actual Lisp code in an editor buffer (say GNU Emacs in a terminal connected to a Lisp process via SLIME) and send the expression for evaluation to a connected or underlying interactive Lisp sessions. The editing of s-expressions in a terminal to move forward, backward, upward, etc is really a non-issue.
Interactive read-eval-print-loops does not mean one is forced to use a terminal without editing support.
Anyways, the problem with handler-case still stands, as well as the other aspects s.a. chunked output, tee and redirects. It's something that would have to be programmed on top of the existing stuff, which was my point originally.
So I added postfix function application. So instead of (f (g x)), you can write (g x | f).
I liked the syntax a lot, but it looked really weird with operators: (calculate x | + 1). So I made operators automatically infix: (calculate x + 1).
I also didn't like that the transformation from foo to (foo :something) (record field access) required going back and adding parentheses before the value, so I added foo.something syntax that means the same thing.
The result is something that's very easy for me to type and read:
(def eyes
(eye-shapes
| color (c + 0.5)
| color (c - (dot normal (normalize eye-target) - 0.72
| clamp -1 0
| step 0))
| color [c.b c.g c.r]))
(Excerpt from the logo of https://toodle.studio -- https://gist.github.com/ianthehenry/612c980f0db04ea3c2ccab27...)Is this even Janet anymore? I dunno. It's a Janet dialect, and it's implemented as regular old Janet macros. But it's much easier for me to type like this. I recognize that it makes my code necessarily single-player, but that's fine for the sorts of dumb projects that I do for fun.
I think a lot of lisp programmers use paredit exactly so that they can write (f (g x)) in the order g x f, but with their editor re-positioning their cursor and taking care of paren-wrapping automatically. But I don't use paredit, and I don't want to wed myself to a particular mode of editing anyway. So I like a syntax that lets me type in the order that I think.
It's really just about readability preference though not ease of editing, lisp like languages will have paredit/sexp editing shortcuts in your editor, so when you're on (f x), you press one key and it's turned into (<cursor here> (f x))
Show HN: Make 3D art in your browser using Lisp and math - https://news.ycombinator.com/item?id=32738654 - Sept 2022 (38 comments)
Janet – a Lisp-like functional, imperative programming language - https://news.ycombinator.com/item?id=28850861 - Oct 2021 (135 comments)
Janet Programming Language - https://news.ycombinator.com/item?id=28255116 - Aug 2021 (114 comments)
Janet: a lightweight, expressive and modern Lisp - https://news.ycombinator.com/item?id=23164614 - May 2020 (269 comments)
Janet – A dynamic language and bytecode VM - https://news.ycombinator.com/item?id=19179963 - Feb 2019 (50 comments)
Janet, a Clojure inspired language for scripting, or embedding in other programs - https://news.ycombinator.com/item?id=19172510 - Feb 2019 (1 comment)
Janet, a bytecode Lisp vm - https://news.ycombinator.com/item?id=18759277 - Dec 2018 (1 comment)
Edit: Back up now
How was the debugging experience?
My apologies in advance, then! (Ha.) The "main" function is at the very bottom of src/joule.janet. So I'd recommend starting there.
As for debugging, Janet embraces REPL-driven development, so the debugging experience is all about setting up, interactively querying, and step-wise updating your program's state, live in memory, using the REPL. It's quite a bit different from a lot of languages—not better, intrinsically, just different. But I like it a lot.
Is your Nim editor public anywhere? I've heard a lot of good things about Nim and wouldn't mind exploring a real-world example.
Maybe I should start looking at that again.
The author (Bill Schottstaedt, Stanford CCRMA) is not too interested in making pretty web pages, ha, but the language is great! https://ccrma.stanford.edu/software/snd/snd/s7.html
It is more of a Python with a LISP syntax.
[^1]: https://janet-lang.org/docs/data_structures/index.html
Closure is not a language so hopefully not?
> the language has matching immutable data structures for mutable ones [^1]
Looking at the lingo being used, the extremely limited breadth of ABI, and the complexity they assert around "immutable" data structures, they're clearly imperative data structures you can't mutate rather than the persistent data structures you'd expect from a strong clj inspiration.
This page looks a lot more like a description of python datatypes than clojure, the only bit that Python lacks is an official frozendict.
Very true, the typo is strong in this one.
> rather than the persistent data structures you'd expect from a strong clj inspiration
What do you mean by persistent here? I assume some kind of shared memory instead of copying? Or something with more practical implications like deep immutability?
> looks a lot more like a description of Python datatypes
I am surprised where this sentiment comes from. Because of the initial comment? Basically any language nowadays has maps, sequences and byte strings. If I had to draw a similarity to Python it would be something along the lines of first-class generators.
I mean the class of data structures called persistent: https://en.wikipedia.org/wiki/Persistent_data_structure
> I assume some kind of shared memory instead of copying?
Sure.
> I am surprised where this sentiment comes from. Because of the initial comment?
Because "tuple" for an immutable sequence is rather specific to Python.
And data structures related to clojure (or functional languages in general) would have some sort of logarithmic component because they'd be tree-based. Pretty much the only O(1) operation in functional data structures is prepending to a linked list.
> Because "tuple" for an immutable sequence is rather specific to Python.
As someone with a background in Swift, Rust and C#, all of which have the concept of a tuple, I did not make that connection, but thanks again.
They all have tuples, but AFAIK in none of them is a tuple a sequence (well not sure about C# it might be there, but I'd be surprised). Usually a tuple is a form of lightweight, anonymous, structure, so it's addressed by field (even if the fields are positions), you usually can't iterate through tuples, or "index" them using runtime variables (except through reflection).
That it is so in Python is somewhat peculiar, but makes sense within the context of the language (unpacking works on iterables, and thus sequences).
Along with the rest of the .NET immutable collection types[2], both are persistent data structures in the sense noted above.
In contrast, .NET tuple types are, as you say, lightweight, anonymous structures addressed by field. The ValueTuple<T1,…> types, in particular, are used in the underlying implementation of the C# tuple language feature[3].
AFAIK, Python has no built-in anonymous mutable structure types, though since type names in Python are basically only used for display purposes, you can easily create them at runtime, e.g.,
def anon(**kwargs):
class _:
__slots__ = tuple(kwargs.keys())
def __repr__(self):
return 'anon(' \
+ ', '.join((f'{i}={getattr(self, i)!r}' \
for i in self.__slots__)) \
+ ')'
def __eq__(self, other):
if not hasattr(other, '__slots__') \
or sorted(self.__slots__) \
!= sorted(other.__slots__):
return False
for i in self.__slots__:
if getattr(self, i) != getattr(other, i):
return False
return True
o = _()
for k,v in kwargs.items():
setattr(o, k, v)
return o
Note that this differs in two notable ways from C# tuples:1. Each object created has a unique type, so
type(anon(x=1, y=2)) != type(anon(x=1, y=2))
More importantly, this means a distinct type object is created and stored for every call to anon, which could have significant performance implications at scale.This could be easily fixed with a cache of already-created anonymous types (trading off slightly increased object-creation time, of course).
2. Equality in the above implementation is based on the equality of identically-named values rather than identically positioned values; in C#, we have
(x: 1, y: 2) != (y: 2, x: 1)
and (x: 1, y: 2) == (y: 1, x: 2),
while in my implementation, anon(x=1, y=2) == anon(y=2, x=1)
and anon(x=1, y=2) != anon(y=1, x=2).
This was by choice, as it seems more intuitive; C# behavior is no more difficult to implement.For read-only anonymous structure types, the Python standard library has namedtuple[4] (which, incidentally, bases value equality on position, not attribute name, so C#'s behavior is arguably more "Pythonic" than my own).
[1] https://learn.microsoft.com/en-us/dotnet/api/system.collecti...
[2] https://learn.microsoft.com/en-us/dotnet/api/system.collecti...
[3] https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
[4] https://docs.python.org/3/library/collections.html#collectio...
It has some of the affordances of the former in terms of semantics, but is stripped down and friendly like Lua.
The use cases also seem to overlap with Lua, as a fast, embeddable scripting language that you can easily keep in your head (see docs).
It seems to be simpler than Lua because it doesn’t complect arrays and dicts into tables, as arrays are a separate construct. And it affords you with immutable versions of those.
To me it looks like a Clojure-like for Lua use cases.
The author also wrote fennel, a lisp/clojure-ish wrapper for lua, so I think that's the influence.
Except they're not immutable in the clojure sense (of persistent data structures), they're just fozen / readonly, as can be seen from their complexity bounds (and the lack of transients). And a quick check didn't show any incompatibility between the two worlds either.
I'm definitely going to try with Janet though.
What cpython type thing? Do you mean a rich(er) set of built-in datatypes?
Because lots of lisps have that, some (e.g. clojure) also have reader macros for pseudo-literals.
Even Scheme has had a built-in hashmap since R6RS (2007), and R5RS implementations usually provided hashmaps even if they were not spec-defined. Common Lisp has had a hashmap more or less all along (at least since before ANSI standardisation)
Why? It's a niche language in a dated syntax, with use cases already covered by other languages.
You will downvote because you don't like that, but it doesn't make it less true.
> jank is a general-purpose programming language which embraces the interactive, value-oriented nature of Clojure as well as the desire for native compilation and minimal runtimes. jank is strongly compatible with Clojure.
(f x) -- too many parentheses
f(x) -- PERFECT
>> f(x) -- PERFECT
They have the same number of parentheses.
f(x) is probably something you have seen for the majority of your life as this is how math is taught.
For anyone who’s spent even a casual amount of time with S-Expressions, the example code is extremely readable. But if ALGOL-like code is your main source of experience, then Janet will look like executable line noise.
(defn sum3
"Solve the 3SUM problem in O(n^2) time."
[s]
(def tab @{})
(def solutions @{})
(def len (length s))
(for k 0 len
(put tab (s k) k))
(for i 0 len
(for j 0 len
(def k (get tab (- 0 (s i) (s j))))
(when (and k (not= k i) (not= k j) (not= i j))
(put solutions {i true j true k true} true))))
solutions)
into def sum3(s) :
"""Solve the 3SUM problem in O(n^2) time."""
tab = {}
solutions = {}
l = len(s)
for k in range(0,l) :
tab[ s[k] ] = k
for i in range(0,l) :
for j in range(0,l) :
k = tab.get( -s[i]-s[j] )
if k and k != i and k != j and i != j :
solutions[ {i:True, j:True, k:True} ] = True
return solutions
pretty much the same. Python is not working because it can't hash dicts, while janet interprets {1:True, 2:True} the same as {2:True,1:True} when these are keys (I think?).In the example janet returns
(map keys (keys solutions))
instead of the "solution" dict that converts dicts like {{1:True, 2:True} : True} into [[1,2]], but I don't get it how.But syntactically janet is not much worse(?).
Typical possibilities:
* ignore
* use for optimizations
* use for type assertions and type checking
That said, Common Lisp is hyper advanced alien technology, so it is hard to improve upon.
Probably better just to get used to prefix notation, though. Just pretend everything is a function call.
D uses Universal Function Call Syntax, where:
f(a)
g(f(a))
can be written as: a.f
a.f.g
It's a very popular feature.Racket, a scheme-based language, extended the syntax so that any symbol appearing between two periods and not at the end of a list gets moved to the head of the list, so (a . b . c) becomes (b a c), which some people use so that (a . + . b) becomes (+ a b).
The D syntax you describe is what schemes call the "threading macro," or the thrush combinator in non-macro contexts.
FWIW it's mostly used for inequalities like `(a . <= . b)` which some people find easier to read than `(<= a b)`.
But `<=` allows more arguments as in `(<= a b c d)` and here the double dot notation can't be used.
Janet has more traditional scoping rules than Lua. Tables and arrays are separate types in Janet, and arrays are 0-indexed. Biggest runtime difference is probably that Janet has value types.
I think the compile-time programming is the real differentiator, but it's hard to summarize what that means in a comment.
Performance is pretty similar to the vanilla Lua interpreter in the benchmarks I've seen and run (Janet typically wins slightly), but there's no LuaJIT.
It requires a much different line of thinking to be applicable in the real world, which goes against historic human nature of following specific instructions, one instruction at a time.
If you're really interested, just try to get through that syntax once and you'll find that it's not that alien.
Perhaps you think the same thing about JS where that was (and maybe still is?) a common idiom. Except that people didn't give those functions names.
And for sure, it's possible to do it in all sorts of languages: python, ruby, js, go, and of course lisp
I get that the syntax is "weird", but honestly, the only two real difference is that the parens are "on the wrong side" for functions/expressions, and the indentation is truncated (the trailing side of the paren pyramid is accumulated and stuck to the end of the last functional line).
Calling non-builtin functions in cPython is super expensive.
)))
it looks like
)
)
)
It's almost the same number of parens, just a different formatting.