1,874 karma · joined July 19, 2010
It's not as fine-grained as allowing libraries specific permissions, but it gets you part of the way there.
I'm not saying that's necessarily the correct design approach, but it's the one that Python has explicitly established as their guiding principle! A different language will take a different approach: Ruby, for example, likes to build multiple alternative ways of accomplishing the same thing, which leads to multiple aliases for built-in methods or various alternative flags that allow a method to behave in a number of ways. It shouldn't be a surprise that Ruby is fine with the equivalent operation:
irb(main):001:0> ["a", "b"].sum(identity="")
=> "ab"
But that's because Ruby's design sensibility is different from Python's. You might like one better than another, but they both come from a consistent vision of how to design programming languages. >>> sum(["a", "b"], start="")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sum() can't sum strings [use ''.join(seq) instead]
I don't think this is incorrect at all! There's a very good reason to use join instead of sum for adding strings together: join is a lot more efficient than simply adding strings together, because it will precompute the amount of space needed for the resulting string at once and then put all the strings into the buffer at once, whereas using sum in the naïve way suggested would do a different append for each element. These are just different tools for different purposes!Could you design a sum that automatically looked at its argument type and deferred to join for strings? Sure! And that'd be a valid design decision as well. But the specific choice Python made here is motivated and defensible: it's only "incorrect" if your personal definition of "correct" is "built with the same algebraic sensibility that Haskell's designers had," but at that point, all other languages are "incorrect" because they're not Haskell!
Some thing it cites are still reasonably true, but others are complaints that reflect the Haskell of a different era. For example, it says, "Even more unfortunate, the applicative functors were introduced to Haskell's standard libraries only after monads and arrows, thus many types are instances of Monad and Arrow classes, but not as many are instances of Applicative." This has not been true for about half a decade, when Applicative was made a superclass of Monad. The article follows with: "There is no special syntax for applicative functors because it is hardly necessary." This is also no longer true, since the introduction of ApplicativeDo: even if you don't like it (and I confess I'm not a huge fan of ApplicativeDo myself!) it turns out that many people do find that sugar easier to write.
However, I think the key point I'd make is: all the advantages listed here of using monad without do-notation are real in some cases, but there's nothing stopping you from not using do-notation sporadically! Occasionally a snippet that uses do-notation can be made much clearer by using >>= or some other monadic function, and occasionally a snippet that uses >>= can be made much clearer by using do-notation, and I don't think there's anything inherently wrong with Haskell keeping both. To make a Python analogy: sometimes you can express a given construct more clearly with a loop than with a list comprehension, but I don't think that list comprehensions should be considered harmful: just use those tools when you need them, and don't use them when you don't need them!
The project is here https://github.com/jonashaag/klaus and you can see a preview of it here: http://klausdemo.lophus.org/
data Set a = Set { contains :: a -> Bool }
which leaves the actual structure of the set undefined. This means you can use a variety of methods 'under the surface' to compute set membership and the external interface can be identical and compatible: emptySet = Set { contains = \_ -> False }
singletonSet x = Set { contains = \y -> x == y }
union l r = Set { contains = \y -> contains l y && contains r y }
fromList lst = Set { contains = \y -> y `elem` lst }
And while this example is a bit trivial, this pattern is invaluable for writing things that OOP can traditionally excel at, like writing GUIs or certain kinds of video games where you want to bundle heterogeneous behavior into a single package: data GameObject = GameObject
{ render :: Pos -> GraphicsContext -> IO ()
, currentPosition :: Pos
, setPosition :: Pos -> GameObject
}
Of course, this is only one (significantly simpler!) encoding of OOP, and it might not be one that you think captures the key ideas of objects, but to the degree that it is it demonstrates that ideas which are common in OOP (like information-hiding behind an interface) can still be useful in Haskell when judiciously applied in the right context.The paper I cited can be read here, which has more details: https://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf
—E. W. Dijkstra
This is a pretty common reason given for why Haskell is "math-ey", but I'd argue it's simultaneously a bit misleading and also a bit boring. The part that's misleading is "Haskell is based on category theory": while a major feature of programming in Haskell (the monad abstraction) was inspired by category theory, the truth is that moment-to-moment programming in Haskell doesn't have much to do with category theory unless you want it to. Even monads require zero knowledge of category theory in order to understand or use! Some people find usefulness in category-theoretic abstractions, but many others—myself included!—don't at all: I've actually improved the performance of Haskell code in the past by cutting out category-theoretic abstractions in favor of simpler code, and by now I steer clear of most Haskell code that wears category theory on its sleeve.
The part that's boring is that "Haskell is based on lambda calculus". The lambda calculus is a mathematical description of a simplified model of computation with functions and binding… and that's basically it. Almost every modern programming language uses variable binding as a basic feature, and in that sense, is "based on lambda calculus". Plenty of other languages, for example, are inspired deeply by Lisps (like JavaScript or Ruby) which in turn were directly inspired by the lambda calculus, but I don't think that lineage makes them particularly math-like. Instead, being "based on lambda calculus" usually just means that they have variables and closures—something true of almost every popular language used today!
Now, Haskell-as-practiced can have a fair bit of math-inspired abstraction in it, which can be daunting to someone new to Haskell. Some of that abstraction is useful (e.g. monads), some of it isn't, but the fact that it exists is more about vocal Haskell programmers and bloggers, and much less about Haskell-the-language being "heavily founded on mathematical theories". And you'll also find plenty of programmers and bloggers arguing against the more math-heavy parts of the ecosystem: just in the past month I've seen plenty of Haskell programmers passing around links like https://www.simplehaskell.org/ which advocates for exactly this less-mathy approach to Haskell.
The bfnightly tutorial is a very good tutorial, but I do want to warn that it's not always terribly idiomatic Rust: Herbert was using the writing of the tutorial as a way of practicing Rust, so there are places where the explanations aren't quite correct and other places where the code isn't what an experienced Rust programmer would write. An example that jumped out at me (this from Chapter 1.7) was the use of
&s.to_string()
which is an expression that could be shortened to just s. (Specifically, s has type &str, calling to_string() on it converts it to a String, and borrowing it allows it to be coerced back to a &str but now with unnecessary copying.) Elsewhere (this from Chapter 1.2) it has explanations like> Copy and Clone allow this [type] to be used as a "value" type (that is, it just passes around the value instead of pointers)
which is also not a correct explanation, or at least is a misleading one: things can still be passed by value even without Copy and Clone and without any pointers at all. (Arguably, he meant, "You can share the value without using borrows," but that's not what the explanation said.)
That said, all this is nitpicky (and I suspect that Herbert would be receptive to feedback along these lines—I just haven't had a chance yet to provide it!) because the tutorial's explanation of roguelike-writing is still very good, and in fact I've been following it as a rough guide for writing a tutorial on top of a different Rust library myself! But I do want to warn about using it as a model for learning Rust specifically.
&s.to_string()
which is an expression that could be shortened to just s. (Specifically, s has type &str, calling to_string() on it converts it to a String, and borrowing it allows it to be coerced back to a &str but now with unnecessary copying.) Elsewhere (this from Chapter 3) it has explanations like> Copy and Clone allow this [type] to be used as a "value" type (that is, it just passes around the value instead of pointers)
which is also not a correct explanation, or at least is a misleading one: things can still be passed by value even without Copy and Clone and without any pointers at all. (Arguably, he meant, "You can share the value without using borrows," but that's not what the explanation said.)
That said, all this is nitpicky (and I suspect that Herbert would be receptive to feedback along these lines—I just haven't had a chance yet to provide it!) because the tutorial's explanation of roguelike-writing is still very good, and in fact I've been following it as a guide for writing a tutorial on top of a different Rust library myself! But I do want to warn about using it as a model for learning Rust specifically.
File::open("foo");
// warning: unused `Result` that must be used
But that's also not necessarily an invalid program, it's just not a very useful one. Failing to open a file doesn't interrupt the program in any way, so File::open here will return either a file handle or an error, neither of which get used. However, if you open a file, don't check for an error, but still try to use the file, then your program won't compile: File::open("foo").read(&mut buffer);
// error: method `read` not found in `Result<File, Error>`
You need to be explicit about what to do in case the file didn't open, and Rust won't let you use your file handle unless you explicitly indicate what to do in that error case! let mut my_file = MyFile::open(path)?;
// Note the `?` above. It's a simple operator that asks
// the compiler to check whether the operation succeeded.
// The *only* way to obtain a `MyFile` object is to
// have a successful `MyFile::open`.
// At this line, `my_file` is a `MyFile`, which means
// that we may use it.These techniques do not "make Python context-free", so the blog's content stands. However, they do help address the final point of this blog post: they provide principled and limited tools which can express this kind of non-context-free grammar in a declarative way. Tools like these allow us to use simple lexers (i.e. without state hacks like Python's) while expressing indentation-based grammars using easy-to-understand-and-analyze BNF-like formalisms.
[1]: https://michaeldadams.org/papers/layout_parsing/ [2]: https://michaeldadams.org/papers/layout_parsing_2/
I get the point you're trying to make, but this feels like a huge stretch. Haskell's list comprehension syntax is straightforward and in many cases a terse and useful way of implementing complex functionality. You'll find multiple uses of it in the base libraries; notably, catMaybes is implemented in terms of list comprehension:
catMaybes :: [Maybe a] -> [a]
catMaybes ls = [x | Just x <- ls]
I'm all for reducing cognitive load, but Haskell has such a sparse syntax and this specific feature is so useful (and honestly, so consistent with the rest of Haskell's syntax) that arguing it's some kind of unnecessary cognitive burden feels ridiculous to me. alignCenter :: [String] -> [String]
alignCenter lines =
let maxLineLength = maximum (map length lines) in
[ leftPadding ++ line
| line <- lines
, let leftoverSpace = maxLineLength - length line
leftPadding = replicate (leftoverSpace `div` 2) ' '
]
You could have verbose and easy-to-read functional code—you don't see this often in Haskell by convention, but you might in OCaml or in Scheme—and you could also have terse OOP code filled with single-letter variable names (less so in Objective C by virtue of the method call syntax, but I've definitely seen code like this in Java and JavaScript.)To be more specific: errors in Rust are either propagated by function return values (usually in the form of `Result` or `Option` types) which do not require any kind of runtime support, or by panicking, which unwinds the stack for clearing-up of resources and then halts the running thread. (Panics cannot be caught, although they can [EDIT: sometimes but not always; see burntsushi's comment below] be observed by a parent thread if they have occurred in a child thread.) Rust's "runtime", therefore is negligible, much like C's: it consists mainly of functions which can be called for things like stack unwinding or backtrace support.
It's also worth noting that if you are using polymorphic functions that are only ever called in your code with a single known type parameter, then your program should be just as efficient as if you wrote a monomorphic version with that fixed type in both runtime _and_ binary size, which means the only disadvantage to the polymorphism there is compile time.
Le sol facto que importa (ab le puncto de vista de interlingua mesme) es que
interlingua, gratias a su ambition de reflecter le homogeneitate cultural e ergo
linguistic del occidente, es capace de render servicios tangibile a iste precise
momento del historia del mundo. Il es per su contributiones actual e non per le
promissas de su adherentes que interlingua vole esser judicate.
(Edit: to translate for non-Romance speakers, this means, "The only important fact (from the point of view of Interlingua itself) is that Interlingua, thanks to its goal of reflecting the cultural—and therefore linguistic—homogeneity of the West, is capable of providing tangible services at this precise moment in the history of the world. It is for its actual contributions, and not the promises of its adherents, that Interlingua wishes to be judged.")It too has met with only limited success: there were some short-lived scientific publications in Interlingua, for example, for the intended audience of all Romance-speaking scientists. Even so, it never attracted much of an audience. That's not to say such a thing could never happen: merely that it's not unreasonable to be pessimistic here.
As a fascinating side-note: the language was also at one point adopted by some kind of fringe pseudo-religious movement called "Psychonetics" as a "…technology of human consciousness." There's a fascinating article about Ithkuil, its creator, and its adoption by that movement here: https://www.newyorker.com/magazine/2012/12/24/utopian-for-be... (and the previous HN discussion of it here https://news.ycombinator.com/item?id=8180493 )
if x = y:
and if x == y:
is very small, and easy to ignore, so insisting on if x := y:
makes it very clear that what's happening won't be mistaken for comparison at a quick glance.[0]: https://www.python.org/dev/peps/pep-0572/#why-not-just-turn-...
(In practice, most—although not all, admittedly—of the Haskell codebases I've worked with tended to be a lot less dense than the occasional combinator-heavy sample code you see in blog posts and comments.)
On the other hand, when you run your code, the sig blocks may or may not be used. There's a lot of machinery in https://github.com/sorbet/sorbet/blob/master/gems/sorbet-run... that handles understanding what a sig means and installing a wrapped version of a method that does type-checking on entry and exit. The intention is that the standalone sorbet executable should be used during development, but it can't catch every error, so the runtime system will double-check types at runtime, which is especially helpful when some parts of your code are typed and other parts are untyped, and control flow passes back and forth between those sections: the runtime will ensure that you don't accidentally pass an object with an unintended runtime type into code with static type expectations.
2H + O -> H2O
JoCaml would write def h() & h() & o() = h2o();;
One of the big differences is that JoCaml allows you to embed payloads inside your atoms, e.g. def a(x) & b(b) = c(if b then x else 0);;
will consume an a with an integer payload and a b with a boolean payload, and produce a c with an integer payload. It also lets you include computations as reactions happen, e.g. def a(x) = ( Printf.printf "Reacting!\n"; b(x + 1) );;
which lets you use this "chemical soup" model as the basis for concurrent programming.There is a very detailed tutorial (which appears to have sadly half-broken formatting) which explains JoCaml in more detail here: https://sites.google.com/site/winitzki/tutorial-on-join-calc...
The Sentinelese language hasn't been studied at all—it's hypothesized that it's related to the nearby Ongan languages (spoken in the same island chain as Sentinel Island) but when Onge speakers have attempted to communicate with the Sentinelese, they haven't been able to understand what's being said. (Which doesn't disprove the hypothesis: English and German spring from a common ancestor, but if an English speaker heard German for the first time, they too would have no idea what was being said!) But as I said above: just because a group is isolated, that doesn't mean that their language isn't changing just as much as others—it might just be changing in different directions!