This looks a lot like the language I've been dreaming about. I actually like the explicit `end` keyword to materialize the end of blocks. The lambda expression syntax, as illustrated by the filter/map/fold example, looks like Ruby blocks with a much simpler syntax. A couple of questions to the crew:
Why advertise it as a teaching language ? As a working programmer this looks very appealing to me. Are there limitations that don't make it a good language for practical programming (other than the fact that it's not ready yet, of course)?
Are you planning to provide a tutorial more approachable than the language reference?
As to why we're advertising it as a teaching language: Our group has a design sense of how to do this, and Pyret's success in the classroom is what we're able to study, measure, and focus on improving. That doesn't mean we won't be thinking about managing million-line codebases or getting great performance out of the runtime, but those concerns won't necessarily be the main guide of our design decisions. We may very well end up with something that's a superb general-purpose language, but I'm not yet willing to give folks that expectation, or do so at the expense of the learning experience. Make sense?
EDIT to add: One thing that the Racket community has very successfully done is separate teaching languages from the main Racket language. A probable future for Pyret is splitting it into teaching and professional versions. I'd love to hear feedback about what more you'd like to see in Pyret, with this split in mind.
The getting started guide (http://www.pyret.org/getting-started/) and tour (http://www.pyret.org/tour/) is the best reference for getting started right now. If you didn't see those then they probably need more prominent placement.
Having said that, it's the first language that I prefer from a typographical perspective to python. So very well done on everything else.
- it's very important for readability
- it should not be semantic
- it should be possible for a program to reindent your code
That is, if I copy-and-paste code from email or a Web page, my environment should be able to "move it into place". Significant whitespace means that that's no longer possible.
Therefore, we want whitespace to be a context-sensitive check that is layered atop the language. You could even see turning this off for code that is machine-generated, sent over a wire, etc. We are looking at actual usage patterns before we decide on the precise rules of indentation.
To this end, we needed some way of indicating that a block had ended. Ergo "end" and its alias, preferred for one-liners, ";".
Let me add one more argument in favor of an ending keyword: quality error messages. The more complex the grammar and hence the parsing job, the much more likely parse errors will be very complex. What we've learned over a few years of doing user-study research into error messages is that ultimately there is a complex algorithm running underneath and the more we can minimize surprise the better, because the user (especially the beginning programmer) simply has no mental model for what that algorithm is doing.
I think the teaching language part is in reference to Captain Teach, an IDE++ that the developers are using in the classes to support code review, automatic backups, and more.
Pyret is being designed with the goal of encouraging good code, with tests being an integral part of every function, and clear differentiation between mutation and simple let bindings. I would teach this language just for those features, so my students would see how easy it is to get lost in spooky action at a distance, or in debugging the wrong portion of your code.
Let me point to testing as an example of where we differ. Testing is really important to us. As you've seen, we have lightweight, in-place test cases. In addition:
1. We have a growing family of language support for writing tests well (e.g., see "satisfies" on the Pyret home page). I expect this to grow richer and richer.
2. We are working on some interesting ideas about the scope of testing blocks. We don't say this on the home page, but you can also write "test" blocks for data definitions, not only for functions. But now the functions that operate over those data are going to want to use instances of the data. We're working on a "scope conduit" to take definitions from the data definition to its functions.
3. As some readers have noted, in most languages, you simply cannot write unit tests for a function nested inside another, because you can't even name it. In Pyret, because definitions can include tests, this creates no problem at all.
4. We are now working on a type-inference approach that uses tests, rather than code, to infer types. The code is then checked against these inferred types.
Essentially, testing is a kind of metaprogramming, and metaprogramming works best when you integrate it into the language (what Racket has shown) rather than as ad hoc external tools. Each of the above four examples is really just a specific instance of this general issue. Since testing is an especially important form of (dynamic) metaprogramming, integrating it well into the language from the very beginning is likely to lead to expressiveness, flexibility, and cleanliness that may be harder to achieve from the outside.
This is part of a more general philosophy. To me, testing is a form of _specification_, and we want to see rich descriptions of program invariants expressed with a gentle blending of static and dynamic methods. We don't want to be so grandiose as to call Pyret a "testing-oriented" programming language, but in our minds it is, with testing having both the base role of making sure we (as programmers) didn't screw up but also the exalted role of providing an alternate, independent description of the program we're trying to write. (This is something I emphasize a lot in my teaching: eg, http://cs.brown.edu/courses/cs019/2012/assignments/sortacle, http://cs.brown.edu/courses/cs019/2012/assignments/oracle).
This is a philosophical position, and so hard to quantify through individual bits of code, but I already see small influences of this (as above) and I expect it to guide us to a very different point in the design space over time. It feels safe to say this is quite different from Scala's viewpoint.
Like, what's the type spec of "map" where the monad argument is a list of even numbers and the function to be lifted maps numbers to uppercase strings? It doesn't seem like we can meaningfully talk about the "type" of the function's result with any sort of specificity beyond List[String] or maybe List[String[Uppercase]], even though the real type is quite a bit more specific.
How does inference work downstream of this point? Can we get reasonable errors before run-time?
Our philosophy is that the typed Pyret language is an explicitly-typed one. Inference is just a convenience to save you some amount of typewritering. We want to do a good enough job of it, and if you want to get more specific, you do it yourself. For instance, in the foreseeable future we do not plan to ever infer a refinement. So I think that addresses your example.
Furthermore, we aren't going to play the cute trick of having an inference process that is so consistent with the type-checker that we can get rid of a type-checker. One nice thing about old fashioned recursive-descent type-checkers is that they give simple and familiar error messages; I don't know of any research papers that have been written about better error reporting by them (at least in the past few decades). That's a feature. [NB: There's always an exception. Hopefully my point is clear.]
So, whatever type we infer is one that gets fed to the type-checker to check against the function. Typically, assuming the function has passed its tests, the function should be consistent with its inferred type, unless the tests didn't cover the code properly.
What happens with code that can apply to multiple types, i.e., parametrically polymorphic code? We made an explicit decision to not have union types; had we had them, this would have been more sticky. But because we don't, we simply assume you mean to use the function in a polymorphic fashion, and that's the type we "infer". [Put differently, if you have a polymorphic function, you should test it on at least two different types!]
For reporting errors, we intend to make a distinction between declared types and inferred ones -- a distinction that I don't believe is commonly made in other type inference error presentations. For instance, inconsistency between two inferred types should perhaps be treated a bit differently than inconsistency between two declared types or between an inferred and a declared type.
More broadly, types are specifications. The point of a spec is to provide a redundant statement of program behavior, that can be checked against an implementation. From that perspective, inferring the type from the implementation is a strange idea: one shouldn't be inferring specs from code. [Yes, I know, it's been fashionable to infer "specs" from code for over a decade now. I would not call those specs -- they're more like summaries of the code.]
But tests, to my mind, are a lightweight form of specification, so it does make sense to infer one specification from another [just as people have gone the other way and used specifications to derive tests: the area of specification-driven test-generation]. Especially since it's being backed up by a true type-checker.
As I said, these are all pretty novel and possibly heretical ideas. But this is the experiment we're trying.