Pen: A programming language for scalable development
github.com
github.com
Took just a bit of searching to find some code examples.
Go back to when you learned your first programming language. EVERYTHING was weird. I honestly don’t understand how after that, brains solidify and think anything after their first year of learning is weird.
I expect that there will be an Emacs/Vi/VSCode/... mode to render them as such.
Is it just me or is the "\" in front of parenthesis (func params) is just off-putting. I wonder, what purpose does it solve?
Familiar syntax makes it much easier to adopt and easily convince devs in your team to try it for real.
\x -> f x
denotes an anonymous function that applies its first argument to `f`.But I have seen at least these forms in other languages of varying popularity:
[x] f x
|x| f x
{ f(it) }
{ x -> f(x) }
fn x => f x
function(x) { return f(x); }
x -> f(x)
(x: A) => f(x)
In other words, I think it is really hard to pick a syntax for this construct that every programmer is going to feel familiar with, especially if your language is supposed to cater both to programmers coming from FP and more traditional languages.Edit: Fixed error in JS example; added Java and Scala.
Imagine if i wrote a conditional like that:
if x > y { return x; } else { return y; }
as { if x > y; return x; } { else; return y; }
It completely erases the usefulness of {}, and is cursed, cursed I say! And I’m looking at you, rust, swift, ruby, etc. list.forEach { x -> ... }
But it becomes harder to read code outside an IDE, especially if the implicit `it` construct is used. (Int, Float) -> Boolean
is written as { i, f -> ... body ... }
but a lambda of type (Pair<Int, Float>) -> Boolean
is written as { (i, f) -> ... body ... }
because `(x, y)` is a destructuring pattern that projects the first and second components into `x` and `y`, respectively. The pattern in the lambda has to use tuple syntax exactly when the type doesn't, and vice versa. Gets me on a weekly basis even though I completely understand what's going on, it's just not very ergonomic. f(5, { square(it) }
can be written as f(5) { square(it) }
which makes constructs like if(condition) { foo() }
Look like if(condition, { foo() } )
as in a function that takes a boolean and a lambda and only executes the lambda if the boolean is true.It's a neat reinterpretation of what {} means.
f(5) { square(it) }
My preference would be to tag the fn signature on the outside of the braces, something like: f(5) [(x)->int]{ square(x) }
I’d also accept ||, (), or nothing as the delimiters around the fn signature. Key point is that it’s OUTSIDE the executable block.I’m not opposed to using some smarts to infer/simplify the expression when possible. I.e. if it’s a closure with inferable parameter and return types the “it” construct could be used (in swift they use $0, $1, $2 etc for unnamed parameters). Just the only thing inside an executable block {} should be code that gets executed - not type information about that block
(lambda (x) (f x))
On the other hand, this is a discussion about syntax, and Lisp is the “syntaxless” language…Slight correction (presuming JavaScript): that’d be `function(x) { return f(x); }`.
#(f %)
or (fn [x] (f x)) x => f x
Or x -> f x \x -> x + 1Despite that, it’s interesting to see a language put dependency injection at the core of the language. I’d be curious to see how that works out in the long term.
The predecessors of C, i.e. B and BCPL also had both kinds of operators but they used the same symbols for them (in B: & | !) and the context determined which was meant.
However the choice made by C for the new symbols was constrained by the ASCII code, which had no other available symbols.
One decade before the C language, there were 6 symbols in use for the 3 logical or bit string operators, for example IBM PL/I used & | and NOT SIGN, while CPL and IBM APL\360 used LOGICAL AND, LOGICAL OR and TILDE OPERATOR.
The names in capitals are the Unicode names of the symbols.
So there are enough traditional symbols for both the McCarthy logical operators and for the bit string operators, without inventing any new symbols, like C did.
In my opinion, the IBM PL/I set of 3 symbols (& | NOT SIGN) is appropriate for the McCarthy logical operators, while the CPL / IBM APL\360 set of 3 symbols is appropriate for the bit string operators.
The reason is that & and | are more visually distinct so they are preferable in IF or WHILE conditions.
On the other hand, the bit string "and" and "or" are nothing else but "min" and "max" applied to vectors of 1-bit numbers. So the symbols LOGICAL AND and LOGICAL OR are also the right choice as symbols for "min" and "max". Because the LOGICAL AND and the LOGICAL OR symbols are just rotated LESS THAN and GREATER THAN, they are graphically suitable for "min" and "max".
—⁂—
I like the generalization you're implicitly suggesting, to use /\ and \/ for generalized lattice "meet" and "join"; bit strings are more naturally a lattice or a Boolean algebra (in the algebraic sense—a distributive complemented lattice) than they are a vector space. There's no analogue to \/ in a vector space! Other useful lattice algebras include integers as you point out ("min"/"max"), integers ("lcm"/"gcd", which is how recent versions of APL interpret ∧/∨, though this seems a bit 05AB1E-ish to me, since how often do you actually use those operations?), floating-point numbers (again min/max), and sets (intersection/union). All of these happen to be distributive in the sense that A /\ (B \/ C) = (A /\ B) \/ (A /\ C) and A \/ (B /\ C) = (A \/ B) /\ (A \/ C). You can also construct lattices as Cartesian products of other lattices and duals of other lattices; these are distributive iff the underlying atomic lattices are. (It might be worthwhile to think of bit strings as a topological space along the lines of the Sierpinski space, but I'm not sure if that enables anything useful.)
If we want /\ and \/ to represent operations on specifically a distributive complemented lattice (a Boolean algebra in the algebraic sense), we unfortunately rule out the min/max and gcd/lcm lattices, as well as finite subsets of an infinite set, because there's no suitable way to define ¬. This seems like a pretty big loss because integer min and max are such common operations, more common than bitwise AND and OR in my experience; infix and looping notation for them makes a lot of algorithms easier to express.
But when we introduce element complementation, there's another operation that becomes natural and is in fact so widely used in programming that Golang has an infix operator for it: abjunction, &^. And abjunction is also an infix operator in SQL (MINUS), as well as a machine instruction in SSE (ANDNPS), ARM (BIC), and Wirth-the-RISC. Because it's falsehood-preserving, it doesn't suffer from the infinity problems that occur in extending ¬ to finite subsets of an infinite set or gcd/lcm, although I'm not sure about min/max. It permits McCarthy-style short-circuit evaluation.
On the other hand, every lattice, whether unbounded, bounded but uncomplemented, or complemented, has a dual lattice; you just interchange meet and join. It's unfortunate that there's no way in most programming languages to use this to derive an efficient min-heap data structure from an efficient max-heap or vice versa.
—⁂—
One of the great benefits of syntax in ASCII (or other small character sets without homographs) is that it diminishes Don Norman's "gulfs of evaluation and execution". If you see an operator in ASCII, even an ugly weird one like #+#, you can probably figure out how to type it on a US English keyboard. (And ASCII characters are common enough that even nontechnical users can usually tell you how to type @ on their non-US-English keyboards.) And, if you've typed it, you can usually tell if you've typed it correctly. By contrast, operators like ¬, ÷, λ, ɑ, 𝛼, ⍺, 𝛂, ꭤ, 𝞪, 𝝰, and α have some real usability drawbacks. (All of those last eight are distinct Unicode characters, but two of them are only very subtly different in the font I'm using here.)
______
† Inexplicably, although the heyday of terminals with hardware character generation only lasted from about 01975 to 01988, overstriking never made a comeback, instead blessing us with the botched abortion that is Unicode combining characters and the fifteen normalization forms.
Depending on your target audience, a language could just not support these at the syntax level. There are all kinds of applications where you never have to do things at the bit level. There's no reason bitwise and/or/not/xor couldn't just be functions. Then you can also use ^ for exponentiation, which depending on the audience may be a more common operation than bitwise xor.
This is too rough but, in other words, dependency injection is simply a less strict or untyped version of effect system or purely functional programming.
I don't consider pointers and mutability to be features, but I consider immutability and trying to hide pointers a feature. One that I don't want. Allocating too much/too often has been like 95% of the performance issues i had.
Computers use pointers and data in ram is mutable. How is pretending that's not the case removing a feature? It's the exact opposite. One of the reasons I like go is that I can have some control over memory when necessary. But most of the time I don't need to think about it unless the profiler tells me that's where the bottleneck is.
Small edit to not be so negative: The language looks interesting and I think it's well presented. Just not for me, and that above irked me in particular.
An algorithm written for such an abstraction is also much more easily adapted to work across multiple machines (e.g. map-reduce).
For example, you could pass all values by reference implicitly, because they can't be mutated.
There are probably others. It seems like the paradigm is different enough that the pitfalls are also slightly different.
[0]: https://github.com/microsoft/mimalloc [1]: https://news.ycombinator.com/item?id=25464354
Which is to say Koka takes 2x as much time as the fastest for that task ?
That it gets continued as a hobby project does not mean it was not funded at some point (and is no longer).
But I'm not sure about my statement; no source also (just remember reading it).
CORRECTION: as can be read rest of the thread, Koka is being funded by MSFT.
I have no idea what it looks like from merely reading the reference docs.
- Everyone can learn the language and participate in actual development quickly.
- Developers can focus on application logic rather than ever-changing implementation details.
So no graphs? Or more complex data structures?
If you are and can hire smart people who can use generics without introducing extra tech debt and complexity, I definitely recommend the ones with generics like Rust and Haskell. But the problem is that I'm not smart enough.
JS isn't exactly the best environment to understand this. Ideally, use a compiler language that can show you the generated assembly (`gcc -S ...` with C++ for instance). Then you can see first-hand how type-specific implementations of a generic function look like.
> Pen is a statically typed functional programming language for application programming with system injection. Its design is heavily inspired by the Go programming language and many functional programming languages like Haskell and Koka.
... Pen aims to be even simpler [than Go] by ... removing several features from Go like pointers, mutability, method syntax, global variables, circular references, etc.
... Pen's approach to that [maintenance] is embracing battle-tested ideas in such field [software engineering], such as Clean Architecture, into the language's principles and ecosystem. One of its most clear incarnations is system injection.
System Injection: A mechanism to inject system functions into pure functions, eg a dynamically typed effect system (https://github.com/pen-lang/pen#system-injection)
everyone: reminder of the very good Show HN guidelines regarding expectations from submissions and those offering feedback: https://news.ycombinator.com/showhn.html