The Pyret Programming Language
pyret.org
pyret.org
Pyret – A language exploring scripting and functional programming - https://news.ycombinator.com/item?id=13185759 - Dec 2016 (267 comments)
A Case for the Pyret Programming Language - https://news.ycombinator.com/item?id=11986977 - June 2016 (37 comments)
Start Coding in Pyret - https://news.ycombinator.com/item?id=9070834 - Feb 2015 (22 comments)
Pyret: A new programming language from the creators of Racket - https://news.ycombinator.com/item?id=6701688 - Nov 2013 (283 comments)
I built a library for doing modular runtime verification in Python (https://github.com/mwshinn/paranoidscientist) and evaluated it for scientific software. In the end, it was pretty effective, but not perfect, and there are some major changes I would make if I were to do this again. One problem was that some of the most important cases to check were important because they were difficult to check. (E.g. some function arguments change the meaning other arguments - this is extremely common in major frameworks like numpy/scipy.) By contrast, the flashiest feature in my package was runtime checking of hyperproperties (i.e. checking properties like monotonicity or concavity that depend on relationships between multiple executions of the function), but this was rarely used in practice.
The two most common criticisms I hear about runtime checking are (a) it is just an assert statement under the hood, and (b) the performance hit is unacceptable and the only solution is static checks. Regarding (a), sure, they may reduce to assert statements, but most idioms in programming also "reduce to something under the hood". The question is whether dynamically-checked (refinement) types/predicates are a useful abstraction, and in my experience, yes they are. Regarding (b), you probably don't want to use runtime checks on software intended to be run primarily by people other than the developers. But many classes of problems, software is written as a means of discovery rather than as a tool for someone else to accomplish a particular task. For these problems, runtime checks and static checks are approximately equally useful. Static checks are nice to avoid because they can get you into deep water really quickly, so their scope can be quite limited. Also, people tend to overestimate the performance penalty of dynamic checks. Even complex checks often incur no more than a 10% performance penalty.
Regarding hyper-properties: I assume they only work on immutable data values, otherwise it would be hard to manage historical objects so that they can be part of any of the predicates.
I'm working on a similar project that you may find interesting: https://odipar.github.io/manikin/. I may want to include hyper-properties in future releases.
edit: found this on hyperproperties: https://lamport.azurewebsites.net/pubs/hyper2.pdf
Yes, you're right that it only works on immutable data types. At first I had implemented something which copies the object, but this used way too much ram and ended up being quite buggy with a lot of edge cases.
I didn't invent the term hyperproperties (sadly). If you do end up implementing hyperproperties in runtime checks, you'll definitely want to look into to doing a statistical approach. The key to making this work is reservoir sampling (https://en.wikipedia.org/wiki/Reservoir_sampling) - there are some more details of how I did this in the Paranoid Scientist paper (https://arxiv.org/abs/1909.00427).
What the parent post talks about is not even close what is described in that PDF above.
"Proving" (or better verifying) stuff at runtime is trivial as it's only some asserts at the end.
Analyzing the properties of programs before you run them is in an entire different league OTOH.
I thought they looked like a genius idea but I don’t get how they work 95% of the time when examples don’t come with all the plumbing required to make valid state for a function to actually work on.
I’m thinking about Python unittest having setUp function for preparing every test without repeating myself. Maybe Rust has a similar way to define setup code for every test.
"Ultimately, Racket’s #lang facilities, though designed to create new languages—and a great prototyping ground for Pyret—proved to not be quite enough to support a language creation process of the scale of Pyret"
I find this rather sad. The PLT group, dogfooding their flagship product's unique selling point, was not able to achieve their goals and switched to Javascript. I hope that one day it may once again be a Racket #lang.
I vaguely get that the idea is an extension of the Unix "shebang" convention to files which define modules. It might be preferable to a system based on file extensions or file associations (it's certainly less brittle). I guess the question is whether it's better than directives of the form "import(lang, file)", in other words, explicitly naming the interpretation language for each file that's imported. Naturally one could set a default so that a plain "import(file)" would do the right thing. A project full of files in extremely heterogenous languages would make make #lang more appealing, but it would also be possible to build some kind of file association system within Racket—personally I would prefer to put the #lang info in the filename or the path than stick it ("intrusively") into the file.
It's an entirely different philosophy, which aims to allow to use "the best tool for the job" even within a project. Need a DSL? Write one!
I guess it seems to me like it hasn't been established whether the "#lang" mechanism is a brilliant invention, or something cheesy. My initial reaction is that it seems cheesy.
On the other hand, I don't think it's a big deal they moved off of Racket. I actually think it's a success story that they were able to build on the platform and then move off.
[0]https://en.wikipedia.org/wiki/Synecdoche [1]https://en.wikipedia.org/wiki/Racket_(programming_language)
I don't see the lack of a JavaScript-backend as failure of the language building facilities of Racket. In fact, the ability to quickly build a new language in Racket, made it possible to try out different features quickly. Also, it made bootstrapping possible.
Switching to Javascript has nothing to do with "scaling". They wanted to go to JS, and simply jettisoned their bootstrapping booster rocket once they got there, because they didn't want Pyret users (including maintainers) to have a dependency on a Racket installation that is otherwise irrelevant to the running code.
This is a slippery slope. Early versions of Dart tried to use rational numbers by default, but then it was cancelled because denominator size can grow exponentially.
Also, I seriously doubt you can make a language that will correctly resolve checks like this one:
sin(pi / 6) == 1 / 2
https://www.wolframalpha.com/input/?i=sin%28pi+%2F+6%29+%3D%...
As one of the maintainers has noted repeatedly on this thread, Pyret is a "real-life programming language" whose purpose is educational. Mathematica is a "real-life programming language" whose purpose is solving math equations. Performance isn't much of an issue in either case, so it's a perfectly acceptable trade-off.
That these languages weren't designed with your use case in mind doesn't make them illegitimate or inferior languages.
With every Mandelbrot iteration* the integers doubled in size, producing exponential complexity. With 100 iterations this essentially means that the program never completes and even runs out of memory at some point.
The newbie-friendly feature turned out to be not so friendly.
* complex z_{n+1} = z_n^2 + c
But we draw limits. Did you try the `sin(pi / 6)` example? You'll find that Pyret produces a rather interesting result that is also instructive. (Hint: it will produce neither true nor false.)
Fun fact about Javascript: there are string and number constants x, y, and z, such that `x < y` and `y < z` and `z < x`. No NaN shenanigans, just regular small strings and numbers. Hint: it involves a string containing digits that starts with "0", thus sometimes getting implicitly coerced to octal.
Think about what this means for sorting!
> Did you try the `sin(pi / 6)` example? You'll find that Pyret produces a rather interesting result that is also instructive.
The result of this doesn't feel like magic and doesn't resemble "JavaScript's truthiness and type coersion" at all IMO.
Here's a live code editor where you can try it out: https://code.pyret.org/editor
I found it to be a very refreshing and intuitively-guiding experience :)
---
Spoiler alert!
So, fist i had to find how to access pi and sin(), which was simple enough googling for documentation. Typing `num-sin(PI / 6)` on he editor already gives an interesting result:
~0.49999999999999994
That ~ there looks suspicious!Then i tried `num-sin(PI / 6) == 1 / 2`, which seems to be syntactically invalid and yields an awesome error message:
Reading this expression errored:
num-sin(PI / 6) == 1 / 2
The == and / operations are at the same grouping level. Add parentheses to group the operations, and make the order of operations clear.
Kudos to the Pyret team for such a nice error message!So finally, adding those missing parens, `num-sin(PI / 6) == (1 / 2)`, yields another informative and useful error message:
Attempted to compare a Roughnum to an Exactnum for equality, which is not allowed:
The left side was:
~0.49999999999999994
The right side was:
0.5
Consider using the within function to compare them instead.
All in all, i'm very impressed with the clarity and approachability of the language. Thanks @skrishnamurthi for the suggestion of trying this out! :D"grouping level" is a cringe-inducing dumbing down of proper terminology. It suggests associativity, but associativity does not have levels. It must be referring to "precedence".
= and / are not at the same level of precedence in basic mathematics!
In elementary school you learn
y = x / 2
without requiring parentheses.It makes sense to require parentheses when exotic, unfamiliar operators are involved for which the precedence and associativity would be hard to remember.
Not for operators drilled into your head by sixth grade.
While I'm generally in favor of rational numbers as a default and believe that its "danger" is no different from that of big integers, there is some truth in this because Guido van Rossum of the Python fame has said the same thing [1]. It seems that you need some adjustments if you are already familiar to languages where rational numbers are not default.
[1] https://python-history.blogspot.com/2009/03/problem-with-int...
Trying to simplify this by removing any stage doesn't really work. sin() over a rational should be a compile time error.
https://dcic-world.org/2021-08-21/index.html
PAPL - https://papl.cs.brown.edu/2020/ is an older version of the current book.
Tweet thread explainer: https://twitter.com/ShriramKMurthi/status/142918126348784435...
o = Object.new
def o.my_method(x)
self.y + x
end
def o.y
10
end
o.my_method(5) == 15 # true
method_as_fun = o.my_method
# Wrong number of arguments, 0 for 1
The last line is actually doing:
method_as_fun = o.my_method()Which should make the error message obvious. Ruby makes this syntax optional for readability purposes, which becomes obvious once you start going through real world ruby code.
method_as_fun = o.method(:my_method)
method_as_fun.call(5) # 15 method = o.method(:my_method)
method.call(5)
# or method[5], or method.(5)
If you really want a "function" that you can call with parens: Kernel.define_method(:method_as_fun, &method)
(Definitely don't do that last bit in a real project...)1: https://yehudakatz.com/2010/02/21/ruby-is-not-a-callable-ori...
Very interesting language.
First a great point about the homepage: it goes directly to business, no marketing buzzwords, no stupid mascot, one glance at that page and any developer gets a very good idea of what that language does and doesn't.
Second yes, they are very interesting choices and functions.
However:
https://github.com/brownplt/pyret-lang
It's implemented in Javascript? This is a kind of no started for the kind of app I do, personally. But as a stand alone language which interpreter would be written in C/C++ and could be dropped into an existing native app, it would be interesting.
But it currently compiles down to JavaScript. Our goal is to create an awesome language for certain educational settings, and many of our users (e.g., schools) are on restricted machines that can't install desktop software. The browser is the one platform they can easily access, and it also saves teachers from turning into sysadmins (to install, upgrade, etc.).
Hence our browser-based delivery. There is also a CLI. And in principle, someone could write a back-end that targets native code, etc. We don't have the resources to do that right now (but it's all Open Source).
Are these types checked anywhere? I think they should be checked at compile-time wherever possible, and optionally at run time too (I say optionally as the checks might make it slow, so they could be checked in development but perhaps not in production).
Do they intend to add this? I think it would make a big difference. It would make thje language more like Haskell, where lots of bugs would be caught at compile time.
> it only checks the top-level type. E.g. `List<Number>` just checks that it's a list.
That's fair enough because otherwise it might have to go though potentially very big data structures.
Maybe there could be a command `strict_check(aDataStructure)` which would only get executed when type checking is on and would recurse into a data structure checking everything.
In any case, I would suggest that a language be powerful and simple enough that it can do a wide range of tasks including teaching children, GUI apps, web apps, AI research, scripting, etc. Python does this and Pyret should aspire to do it too.
What it should do is a decision that should be made by the individual curriculum/instructor.
https://dcic-world.org/2021-08-21/part_appendix.html#%28part...
For tests, NGS has "TEST" keyword which can be placed at beginning of any line. The test is everything till end of that line. I typically place these below functions that they test.
> # this is true
> ((1 / 3) * 3) == 1
This worries me. I know it is true mathamatically, but experience tells me trusting floats is a recipe for disaster. A approximate equal operator is safer.
Floats are such a leaky abstraction it is in my opinion better students know this early on.
> Pyret has numbers, because we believe an 8GB machine should not limit students to using just 32 bits.
Emphasis mine. WTF, author.
But the Java sample below saying "this is false" implies that floats are a problem that is being avoided. The only reasonable solution is rationals... right?
If you go into the editor/REPL (https://code.pyret.org/editor) and enter 1/3, you'll see that it gets rendered as 0.3 repeating (with a bar, not some arbitrary rounding) or if you click on it, directly as 1/3.
1/3
As a rational type, and do not convert to float unless you cast. Common Lisp comes immediately to mind. Evidently pyret is another.The same in other languages: function, func, fun, fn, etc.
We were well aware of the Haskell use, but most of our users have not seen or even heard of Haskell before. So that's not a real problem here.
People who don't have Haskell experience find it utterly unsurprising that what follows `where` are the examples/tests. So I think your expectation is being set overly by your Haskell experience.
lam(actual): num-abs(actual - target) < delta end
This is a new, even worse kind of "whitespace significance" than indentation. Is it a function called `num-abs` or a variable `num` minus `abs(actual - target)`? I can't tell if the language would allow `actual-target` as subtraction.In practice I doubt this would trip me up because every coding standard I've worked with has mandated spaces around infix operators. I suppose it might be a lot more confusing if you're used to coding standards that allow you to omit them, but my understanding is that's a minority.
> "Pyret is a programming language designed to serve as an outstanding choice for programming education"
Pyret is the result of decades of research in computer science education, helmed by Shriram Krishnamurthi, who was one of the original members of the Racket project (itself a language designed for CS education in the '90s, a descendant of Scheme with kebab case and prefix notation). The full list of authors of Pyret is lengthy, but includes a number of well-established researchers in the PL and CSed communities. Knowing who they are, I would happily assume that they spent plenty of time debating this exact issue of mixing kebab case with infix subtraction, and either decided the benefits were worth the cost or else decided that the cost was practically nonexistent.
In any case, I trust their decisions better than those of someone who glanced at the website only long enough to inform a condescending (and highly superficial) comment on Hacker News.
Beginners often find both the things you are saying some what hard.
First, note that there is reading vs editing. Perhaps in grade school people learn to parse large expressions (though I find that people generally are not prepared well, because grade school expressions are too small). But editing code and keeping track of what changes you made --- essential to debug your first programs --- is much more new to people who are used to just rewriting a few short algebra expresions with pencils. It's then, when the beginner is most mentally taxed, that the syntax errors creep in --- and further interrupt their thinking process.
Hopefully we can agree the second challenge is completely fundamental to the field, while the first however is just an artifact of the way things are implemented today. Well, based on the above scenario and others I repeated see the cognitive burden of the first interrupting the second, and so student waste effort and loose focus over "easy stupid text syntax", and therefore long delay mastery of trees, term rewriting and substitution in particular, etc.
It might be easier to explain, but it also makes the code look ugly. Consider the expressions:
a := b + c*d
if e > f-g then ...
Here the * and - are more tighly bound than the + or >, and I'm using the whitespace to make that obvious.Then allow everyone to do things according to their own aesthetics.
We're enforcing whitespace around infix operators in Vale, and it's working pretty well. It's also enabling us to use <> for generics with no ambiguity =)
It should say base*exponent.
I think other languages use base^exponent, which would be fine with me too.
a := b+c * d
you aren't going to get an error. You're just going to get awfully surprising behavior, because you may think you were expressing one precedence with the spaces but the language has its own mind and doesn't care.In contrast, Pyret doesn't bind anything more tightly than anything else. You parenthesize to make your intent clear.
If your expression gets too large, you should consider breaking it down with names for the intermediates. That will improve its readability by others anyway.
But this is a bad argument, as it could apply to any language feature. E.g. I could write a program:
def add(a, b):
return a - b
Here I've called it "add" but it does subtraction. By your argument, we should ban functions having meaningful names, as people could use a misleading one.> Pyret doesn't bind anything more tightly than anything else
So all dyadic operators have the same precedence, like in Smalltalk? How horrible.
> You parenthesize to make your intent clear.
More likely I write my code in something other than Pyret, to make my intent clear.
I didn't interpret it as condescending.
> (and highly superficial)
I didn't interpret it as highly superficial. Comments about the appearance (including whitespace, kebab-case, and infix operators) of a language are fair game, particularly if it is intended for teaching.
On one hand, this historical context is relevant. Thanks for sharing it.
On the other hand, when this section is taken in context with with the following sentence...
> Knowing who they are, I would happily assume that they spent plenty of time debating this exact issue of mixing kebab case with infix subtraction, and either decided the benefits were worth the cost or else decided that the cost was practically nonexistent.
...it looks the fallacy of appealing to authority.
Many people like to debate the ideas and tradeoffs, not the credentials of people involved.
We don't need to assume when we can search. I did a few minutes of searching and found the following:
https://groups.google.com/g/pyret-discuss/c/rPe7gYBLdPs/m/3O...
Shiriam K. wrote in 2013:
> I have tried just about every possible experiment to stay closer to Lisp, including even the one you have in Wart. None of them scaled well for me. Ultimately, also, I am totally unconvinced about the idea of having an identifier named e^ipi-1 for anything other than cute illustration purposes. Since surface* syntax is designed for humans, I think a compromise between expressiveness, readability, and predictability is a good way to go.
(This is just one comment, I'm not saying that is captures the full thinking and discussion around these syntax decisions.)
It takes a lot to make a choice for reasons other than your own familiarity. From the linked thread, they clearly agonized over this. Apparently they talked themselves down from the usual Lisp ultra-permissiveness on idents, so good on them -- but it was still explicitly a compromise between their comfort and the aims of the project.
There is an obvious and very good reason why the number of languages that do this can be counted on one hand. The authors know that. It is a problem they decided to accept because it was the default for them, and they "just couldn't give up on how pleasing hyphens in identifiers are to the eye and the shift-finger", and then mitigated it via other whitespace changes. They were content when "Anecdotally, no one in our courses ha[d] complained". I disagree with it. I find it really hard to read. I guess I am used to other languages, but so is everyone who has dabbled in python, likely writing primarily numeric code for their stats/bio/physics courses.
Consider me a student reporting it as a problem. Consider the 2013 thread a student reporting it as a problem as well. How about that?
I'll finish with a quote from Krishnamurthi himself: "Saying 'add spaces around binops' is easy to learn, recognize, and implement."
Does this not speak for itself?
fun subtract(a, b):
a-b
end
>>> The identifier a-b is unbound: definitions://:3:2-3:5
4 | a-b
It is used but not previously defined.
Edit: I dug up an old thread from the guy @estebank who makes all the amazing Rust error messages, responding to a paper of Krishnamurthi's that argues, true to form, that you shouldn't ever actually "Say 'add spaces around binops'" (https://twitter.com/ekuber/status/1140791186858266624). The paper itself is an interesting read, particularly the interviews where beginners don't have the vocabulary to decipher error messages. But I think Esteban is right.But I stand by my remarks from 2013 in 2021. I've now had well over 1000 students go through Pyret (in addition to tens of thousands elsewhere). We've also spent hours and hours literally watching new learners work with the language. I can assure you that of the many, many, many issues that have percolated up to us, spaces-around-binops has literally not a single time been one. If anything, when people write that and we say "just put spaces around the `-`", the response is, "Oh, okay", and people move on.
So, we feel very good about this decision. And I, personally, actually really like how it makes code read.
On the binops, it may be that forcing whitespace around them is a good idea even for a language without kebab-idents, and perhaps the risk you took there was worth it to find that out. I could get around a world in which C's `a & b` and `&ptr` were completely incompatible. (I'm sure you could find a CVE or two for that one.) Heck, every code formatter out there does it. Compared to some suggestions around here and apparently back in 2013 as well to ditch BODMAS, the one bit of math that everyone on the planet learns in school, in favour of whitespace-sensitive precedence... goodness me. Give me forced spaces around binops any day.
If not, this does:
$ cp a-b c-d # copy a-b file to c-d
$ expr 1-1
1-1
$ expr 1 - 1
0
You already know some language with whitespace delimiting such as, oh, the shell. sub foo-bar { 10 }
sub bar-foo { 7 }
say foo-bar-bar-foo; # Undeclared routine error
say foo-bar()-bar-foo; # 3 say foo-bar - bar-foo;
which would improve readability as well!When you are designing your own language, you can make these choices. The author of Pyret obviously thought that clearly named identifiers were worth it.
Indeed. What's wrong with using _ in identifiers?
Nothing wrong with it. It is usually easier to type '-' though. Use in when naming scripts and files for the same reason. Ergonomics matter IMHO.
fun to-celsius(f):
(f - 32) * (5 / 9)
end
It looks like abbreviated python. (* (- f 32) (/ 5 9))And some Ruby with the end keyword to close blocks.
That said I don't like C++'s library additions just to stay modern. They are not expressive, increase build times, make debug builds slower and can fail compiler optimizers.