Fantastic idea! I'll keep that in mind, should be fairly easy to extend Lisps or other AST-Macro enabled languages (Elixir, Julia, Python) with such a functionality. I really like that. It makes it easy to work on a function and code tests down without switching contexts (files, workspaces, etc)
Tests are a form of documentation, but too much in the code, like too many comments, obscures. Higher level unit tests (acceptance tests) can be quite long, especially if there's a lot of setup - unlike their example code. Literate programming tried embedded documentation, but didn't catch on (even with Knuth's backing). Embedding tests makes them easier to keep in sync, but tests are already kept in sync by (hopefully) failing when not. However, their idea of automatically running tests is interesting, so there's no infrastructure to set-up, run etc (though you'd want to be able to disable them).
Nice for teaching.
Literate programming is not "embedding documentation". The main idea was to separate the order in which the code is read from the order in which the compiler sees it, and it embeds the code in the documentation, not vice versa. It was a very idiosyncratic thing, hard to imagine a team of programmers in a typical current commercial setting, developing a nice LaTeX essay around the actual code of the next social network web-app. As the program grows larger it also gets harder and harder to maintain the "story" around it.
However, the `where` tests play into the type-inference story for the language!
Code folding in a IDE goes a long way to make this bearable.
I would argue literate programming is experiencing a renaissance, thanks to docco:
And as other people mentioned, you can always use the check statement out of band, too.
Modern IDEs/code editors can support code folding as well as projections fairly easily. I don't see a problem.
Of course it's not equivalent, but Python doctest module at least allows you to keep tests close to the code. Here's Pyret example, ported to doctest:
def sum(l):
"""
>>> sum([])
0
>>> sum([1, 2, 3])
6
"""
return reduce(lambda x, y: x+y, l, 0)
if __name__ == "__main__":
import doctest; doctest.testmod()It's been tried many times before and it has always failed. A few reasons:
- It clutters the code
- If you need simple tests, asserts suffice
- If you need more sophisticated tests, write functional tests, separately
The approach offered by Pyret (along with similar ones, such as design by contract) are compromises that are the worst of both worlds.
http://en.wikipedia.org/wiki/Eiffel_(programming_language)#D...
(with-test
(defn my-function [x y]
(+ x y))
(is (= 4 (my-function 2 2)))
(is (= 7 (my-function 3 4))))What?! You can't write macros in Python? (right? have I missed something?)
That said, Pyret appears to be aimed at education so this may make more sense.
Put the tests and docs in with the code and you end up going into "scanning" mode. The actionable information density per character is lower.
Put the tests and docs outside the code and you (or that obnoxious guy who just changed > to >=) don't update the tests or the docs. In the worst case (cough functional web tests) people tend to disable the tests because it's too much work to get them to conform to the changes and everyone is on fire and babies are dying and this needs to be done in production now.
Does anyone have any experience with workflows like this? Are tests next to code a pragmatic technique?
Cool? Yup! Good for teaching? Probably not.
Take a look at the grammar and judge for yourself: https://github.com/brownplt/pyret-lang/blob/master/src/lang/...
I have two responses:
1. You're right that exposing too much, too soon is a recipe for disaster. People can only fit so many new concepts in their head at once, and blasting them with refinement types from day one isn't a great idea. However, there's two things at work here: the role of the curriculum and the role of the language. Pyret is careful to allow a gradual transition to more and more complex features. Annotations are not necessary on day 1, and neither are iterators. We write untyped, recursive-descent functions on lists in the beginning, and build up to types and the pleasant terseness of "for" loops. If Pyret required introducing these concepts to write any programs, that would indeed be a problem, but we've put thought into the dependencies between curricular concepts and the needed language constructs to mitigate exactly this concern.
2. I think that some of the features you listed are actually a huge necessity in early programming, particularly special purpose syntax for data structures. Teaching the definition and implementation of a balanced binary tree to folks without language experience in Python or Java requires a huge amount of boilerplate explanation. You need to describe classes, fields, and constructors just to do the equivalent of Pyret's "data". The alternative (at least in Python) is to encode everything in lists and dictionaries, but hopefully in 2013 we've moved beyond writing all our data structures that way :-)
"He speculated that the size of programming languages might confuse many students trying to learn to program. Java, the teacher’s programming language of choice at the end of the 20th century and at the beginning of the 21st, is defined loosely but at great length in several hundred pages.4 Natural Deduction, on the other hand, can be defined precisely on a single side of a single A4 sheet, in a normal font size. It is small and conceptually simple: surely it can be taught! Well, up to a point it can, but the results were depressingly familiar. Figure 14 shows the results from an in-course exam on formal logical proof. Unlike the examinations of section 3, students were strongly discouraged from guessing, with negative marks for wrong answers in multiple-choice questions and extensive unseen proof problems. As a consequence, two humps are clearly visible, with one at 33-40 (about 50%) and another at 65-72 (about 85%). (This data was collected long before Dehnadi’s test was developed, so we can’t analyse it further)."
Saeed Dehnadi - The camel has two humps (working title): http://mrss.dokoda.jp/r/http://www.eis.mdx.ac.uk/research/Ph...
It's nice, but I'm not sure I'd go with amazing. Most or all of Niklaus Wirth's languages have shorter grammars, for example. I'd imagine quite a few others do as well.
E.g. Oberon-2 has 33 EBNF productions: http://en.wikipedia.org/wiki/Oberon-2_(programming_language) - that fits fully on screen for me with my default font size...
Regarding Ruby, I agree. I'm working on a Ruby compiler, and "decoding" the MRI parser into something closer to optimal will still leave an awful mess no matter what you do. I love programming Ruby, but implementing it is hell (and one of the reasons I'm trying it...)
Smalltalk would like to have a word with you: http://chronos-st.blogspot.be/2007/12/smalltalk-in-one-page....
Our goal is to eventually create language levels, pioneered by DrRacket, based on what we learn from observation. This is a research project in addition to a development one, where the research is into human factors.
Refinement types are nice. I really wish those were more common.
So it's syntactic complexity might be high, but it's conceptual complexity is rather low.
Oz is one of the best languages ever for learning CS.
Presumably it is easier to learn about concepts with a language that has them.
But, as you state, there are a lot of higher-level programming concepts you can't effectively learn in assembler (one imagines a bizarrely circuitous route around learning to build compilers in assembly (via Forth perhaps) in order to teach lexical scoping or similar).
I think Python is a very accessible programming language that still has enough depth to go into a lot of interesting CS subjects. That Python doesn't have manual memory management or support for true multiple inheritance means that you can't teach those things effectively with Python, and I'm completely OK with that trade-off.
I mean, even if you look at how language like Scheme/Racket, or Python, or pretty much any other language is used in teaching, you don't usually use the whole thing in any one teaching context.
DrRacket pioneered the notion of "language levels" that grow with the student's needs. Pyret will end up taking some variant of this route. It's just too early to design those levels yet.
http://cs.brown.edu/~sk/Publications/Papers/Published/mfk-va...
One issue is that it's too regular: since open-paren means so many different things (start of a "defun", start of a function application, start of an argument list in a "defun", start of a syntactic form like "cond" or "if", start of a clause of "code", the list goes on), a typo can easily and drastically change the kind of error message you get.
More closely matching syntactic forms to the type of behavior the expression has will hopefully let us improve error messages and grokkability of the difference between concepts. We're collecting data about common syntax errors and actively asking what we can do better in syntax design based on what we observe about Pyret's use.
As someone who hasn't spent much of my time in Lisp world, most of my time when I come across some supposedly elegant implementation of an algorithm in Lisp, what my brain sees is impenetrable parenthesis soup. One incredible win of Python is the resemblance of the syntax to pseudocode, and how easy that has apparently been for new developers to absorb. I'm glad to see your team has taken that to heart for a language that's supposed to be pedagogical. And that is beautifully said about how a misplaced parenthesis can lead to all sorts of errors.
Error reporting, debugging, and documentation are in that "meta" tier of programming ergonomics that few people care to reason about, and yet they are oh so important to allowing actual human beings to learn programming languages and use them to produce rereadable and maintainable code.
Lisp has the advantage that it really teaches the general semantics of programming languages, because it's syntax is just the syntax tree, and it basically only has one form, which is function application. There's some "special forms" built into the implementation of a lisp to make it useful, but those shouldn't be confused with syntax. In fact, the parenthesis and space between function and arguments shouldn't be considered the lisp syntax either - it's just one way to represent a tree structure in linear text.
I think the real issue is the confusion in teaching is between Computer Science and Computing as a vocation. Nobody teaches the former any more, because it's less useful in the real world. As a result, we have languages which try to make a distinction between "programmer" and "programming language implementer", where the programmer generally knows less about what he is doing because someone has imposed a specific, narrow set of ideas on him.
The course later moves into 3 other languages (OCaml, Scala, and Java) to try and get the beginners not focused on any particular language, though there is some debate on whether that effectively teaches the beginners any language particularly well.
Pyret would definitely be a candidate for replacing OCaml in that sequence as anything with better error messages would be very welcome.
The simplicity of parens is widely regarded as a strength, but I believe it is also a weakness. There is too much regularity in the syntax, resulting in numerous errors. Programmers, especially beginning programmers, need more "guideposts" in their programs. Additionally, parsers also benefit from this when producing errors.
The syntax is basically Ruby + Python + Haskell. Each of those languages has a lighter, more intuitive and memorable syntax.
Why would the syntax be:
data BinTree:
| leaf
| node(value, left, right)
end
Instead of just data BinTree = leaf | node(value, left, right)
The whole colon thing in Python is a mistake, it should have never been in Python, and it definitely not be repeated in other languages..1. Pyret comes out of Shriram's group's expertise with pinning down exactly what Python and other dynamic scripting languages do right, and (mostly) do wrong. Check out their recent paper Python: The Full Monty: A Tested Semantics for the Python Programming Language http://cs.brown.edu/~sk/Publications/Papers/Published/pmmwpl... for more context.
2. Ruby is far and away not the originator of 'end' to end blocks -- this comes from Pascal and is in other languages that have nothing to do with Ruby, like Lua.
3. Languages that aren't Haskell have ADTs too; it happens that they've lifted an ML-style syntax for defining them.
4. Python is widely used pedagogically, so for better or for worse, students are already being familiarized with the colon, which I agree is a bit anomalous.
The use of 'end' comes from Algol, not Pascal.
data BinTree: | leaf | node(value, left, right);
We use "end" or ";" in order to have unambiguous delimiters for the ends of syntactic forms and avoid needing to depend on whitespace (we added some discussion on pyret.org about our philosophy on indentation and why we don't want to depend on whitespace).Making that leading pipe optional is a good idea. I made an issue for it, we'll think about if it'll break or confuse anything and add it if it doesn't:
https://github.com/brownplt/pyret-lang/issues/106 check:
4 + 5 is 9
1 / 3 is 2 / 6
9 - 3 is 6
5 > 4 is true
end
Then becomes, not wrapped in end-check, but: check(arithmethic-test):
4 + 5 is 9
1 / 3 is 2 / 6
9 - 3 is 6
5 > 4 is true
end(arithmethic-test)
Or something similiar. This makes mis-matching "end"s (either typos or
artifacts from cut'n'paste coding) explicit errors that are easy to
spot, and identify.It would add a lot of verbosity, of course. As for ";"/"end", consider (the presumably valid):
check:
4 + 5 is 9
1 / 3 is 2 / 6
9 - 3 is 6
5 > 4 is true
;
That trailing ";" is going to trip someone up. Also consider
(cut'n'paste-with-quick-edit): check:
4 + 5 is 9
1 / 3 is 2 / 6;
9 - 3 is 6
5 > 4 is true;
end
Is this valid?Anyway, it seems to me that the only real difference between your code examples is that the second one lacks an ending designator and uses an equals-sign instead of a colon. However, in order to make the lack of an ending designator work, you need a more complex parser (it needs to infer block-ends from indention or some other context). Leaving off ending designators also increases mental overhead for the user, as they must keep block-end inference rules in mind in writing code. Using colons versus using equals-signs is simply a matter of taste (not weight, as you seem to claim).
That said, I do see some waste in their datatype definition syntax. First, I'd prefer curly braces over the colon/end pairs that they went with, as that saves a few characters. Second, newlines are a fine separator, so why also require pipes? This requires the user to type three characters (newline, pipe, space after pipe) when just one would have done fine.
I admit to also being a brace weenie; I would very much prefer if all the languages I had to use had them. However, Pyret exists in a tradition of many successful, braceless languages like Python, ML, and Lua.
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.
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.
1. Identify the data - create data definitions (You are gixen x and expected to produce y)
2. Write concrete examples of the data (This is hard and takes time)
3. Write contract, purpose, header for functions (contract and header are annotations in Pyret, purpose should be a commented statement)
4. Write concrete examples of the function (This is hard and takes time. This means test cases!)
5. Write the template (This may only apply to recursion in Racket, but the idea is if you're dealing with a cons, you always have the same structure of checking if a cons? or empty? and must recur)
6. Fill in the template (ie, complete the function)
fun f(l :: List<T>):
cases (List) l:
| empty => ...
| link(f, r) => ... f ... f(r) ...
end
end
Note that because you can put type annotations in the cases statement, you can remind yourself of the type of the locals: fun f(l :: List<T>):
cases (List) l:
| empty => ...
| link(f :: T, r :: List<T>) => ... f ... f(r) ...
end
end
which further nudges you towards a possibly recursive solution.Instead: "end" syntax, unified number type, all blocks produce new scopes (not just functions), local variables are explicit instead of default, etc.
The note about local variables is particularly important; we actively don't want Python's model of variables and scope.
The language itself looks pretty slick, and I am a sucker for gradual/optional typing. Do the type annotations result in performance/compiler optimization, as in Julia?
I've been burned by multiple return values numerous times in Scheme and Racket. They're very subtle and hard to make performant. It's one of those language features that very much does _not_ pay its own way, so you have to really, really need it to want to put it in your language. My view is that its uses are not many, and most (all?) can easily be achieved with literal objects. That's why Pyret doesn't have them and is unlikely to get them.
fun square(n): n * nRuby intentionally makes parenthesis optional. So `do_something` is `do_something()`.
For the first example,
- method_as_fun = o.my-method
- method_as_fun(5) # not reached
+ method_as_fun = o.method(:my_method)
+ method_as_fun.call(5) # or method_as_fun[5]
And for the lexical scope thing, def f(x)
g = ->y {x + y}
g[2]
end
f(2)
Or, explicitly use class variables, def f(x)
@x = x
def g(y); @x + y; end
g(2)
end
f(2) 1. common (not all) things are simple / elegant.
2. Advanced things are doable, usually require a slightly more complex syntax.
For example, Ruby allows one "block" (anonymous function) per method. Why not 2 or more blocks? Because Matz has studied common lisp (likely, maybe something else) standard library and noticed that in ~97% (maybe not exact number) cases, one anonymous function is enough. But you can use more anonymous functions using a different syntax.And here is the same case. People call methods much more often than to get the method. Therefore Ruby makes it default to call the method, but not stop you from getting the method object. It's just unfair to say Ruby cannot do these things.
I loathe template-based OO due to the possibility of monkey-patching but the fact that objects are immutable looks like it might ease this. (Generating efficient code might still be difficult.)
I don't see the use cases for the cyclic structure as presented. If your data at all resembles a database relation, then (a) you have scalar keys anyway so you don't need language support, and (b) you probably need to key off of more than one column.
The "method-as-fun" semantics seem weird. Given this code:
o = { x(self): self.y end, y: 10 }
f = o.x
f() returns 10, as per the examples on the front page. But then: p = o.{y: 15}
q = { x: o.x, y: 15 }
Clearly p.x() should return 15, but what does q.x() return? It doesn't seem clear whether it should return 10 or 15. (Without reading through the reference manual (it's late), my guess would be that method definition is special-cased, so that the answer would be 10.)We don't have monkey-patching. We've been burned too much by JavaScript and the like.
Cyclic structures: think about trying to teach graph algorithms. Let's say you believe it's important to write tests. Many graph algorithms aren't inherently mutational. But you need mutation just to create examples of your data. The graph construct gets around this problem entirely, so mutation and graph algorithms become orthogonal topics. In general, I'm a big believer in eliminating unnecessary curricular dependencies. Having taught graph algorithms this way for a few years now, I can't imagine going back.
See the notes on graphs in PAPL (papl.cs.brown.edu/2013/).
OK, I'll buy this.
I coded in Java for many years and Ruby for the last several, the lack of explicit type checking in method signatures or via annotations built into Ruby has not gotten in the way enough where I felt I needed to add something to decorate methods to do some generic form of type checking in Ruby. When I really need to check the type of an object sent into a method, it is typically a method that can handle different types, and in that case, I'll use respond_to?(...) to see that an object passed in as an argument responds to a method, use a case statement, is_a?(...), etc. but that is certainly not on every method- probably more like 1 in 20-30 methods.
Also, in the comparison section of the doc, OCaml and Python were represented, but not Ruby. As of late 2013, there are more jobs containing "Ruby" in the job description than "OCaml": http://www.indeed.com/jobtrends?q=ocaml%2C+ruby&l= So, imo it should give Ruby some love with a comparison.
method_as_fun = o.my-method
which should be:
method_as_fun = o.my_method
and the examples themselves just look a little crazy, imo. Not crazy because of what you are trying to do, but in how you are trying to do it. What was the intent of calling a method that defines an argument without an argument? That's not a problem of the language; that's just an error in coding. There are all kinds of things in Ruby to handle method definition. Arguments can have defaults. You can use splat and unsplat to handle unspecified arguments and composing arguments of various # on the fly. You can pass in blocks specifically (&something) or optionally (yield, etc.). Procs allow argument sillyness and returning the parent method by explicit return in the proc body, lambdas don't, etc. Ruby is a great language, and you should give it a college try for several months to get the hang of it.
By the same token, I have no clue what you are trying to do here:
def f(x); def g(y); x + y; end; g(2); end; f(2) # undefined local variable or method `x'
You can define methods on object instances, if that is what you are getting at (define_method/class_eval/etc.), and getting familiar with blocks, procs/lambdas might help. It isn't JavaScript, but I can't think of that much that I couldn't do in Ruby (barring high performance, compile-time type checking, etc.)
If Racket people "hate types", you'd have a hard time explaining the existence of Typed Racket, or that some of the most cited research on types is by one of Racket's creators.
But please, don't let facts get in the way. (-:
PEP 3107 introduced function annotations to Python 3. The following syntax is valid:
>>> def square(n: int) -> int:
... return n * n
...
>>> square(3)
9
Nothing is done with annotations by default. Here's an article discussing this "unused feature": http://ceronman.com/2013/03/12/a-powerful-unused-feature-of-...But please remove the minus sign from identifiers! I am concerned that people may abstain from Pyret for this simple reason. It is really annoying to embrace every operator with blanks. That should be an optional job for the IDE to increase readibility.
By the way, is there already a raco exe for Pyret?
We're sticking with the identifier syntax for now. We've written a lot of code in Pyret over the course of 9-12 months and we really like it. My guess is that people will only notice it if someone points it out (only one person on HN noticed, and even that was to ask how it could work). Most folks will just get on with the job at hand.
In short, though, sorry, we can't commit to helping you interop with your existing Racket code. But you're already in Racket, so life is probably as good as it's ever going to get. (-:
That said, the title of this post is a bit misleading so I wanted to correct it. The group of people who develop Racket and Pyret are mostly disjoint. I'm not trying to diminish Pyret at all, but wanted to make sure the right people get the credit. You can find out who develops Pyret here: http://www.pyret.org/crew/
* ML-like syntax for algebraic datatypes and matching. ML got it right; it always seems a bit off when languages try to make ADTs look like some other syntactic construct
* the cyclic and graph declarations
* accessing datatype variants using an OO-like syntax. simply brilliant.
* non-significant whitespace. for all the pros and cons, autoindenting is something i hate to give up.
-.-
> fun to-celsius(f):
> (f * (5 / 9)) - 32
> end
Wait a minute..