Mars: a hybrid imperative-declarative higher-order language
mars-lang.appspot.com
mars-lang.appspot.com
Languages like Common Lisp and Dylan (and SML) have mixed imperative and functional paradigms in a non-controlled way for awhile, but there is some interesting work going on about how to do it in a more controlled way. Much of this work involves limiting aliasing and mutability, or expressing imperative features an effect system, or some combination of the two.
Rust is probably the most mainstream example of the former, but Mezzo is another language exploring this area: http://protz.github.io/mezzo. The basic idea is that if you've got the unique reference to an object, or a tree of objects, you can modify it without worrying about how it will affect the rest of the program. Koka is a language that focuses more on the latter, expressing the imperative characteristics of a function through an effect system: http://arxiv.org/pdf/1406.2061.pdf (section 2.1). The basic idea here is similar to Java's exception specifications. If the set of objects potentially modified by a function must be part of its type, then you can use the type system to keep you from modifying an object, or calling a function that does, in a context where the rest of the program doesn't expect that object to be modified.
Monadic types can be inferred most of the time in Haskell, it's just considered good practice to include a type signature for every function.
>Koka also allows you to mix effects, e.g. a function can be `<io, st<h>, ndet>`
This can be done with Monad transformers, although it's a little awkward at the moment. There are various proposals for better composition here, and I suspect sooner or later someone will work out a good solution.
>`map` can be polymorphic in the effect of the mapping function.
`mapM` is polymorphic over monadic functions. `map` doesn't handle monadic functions (unless you just want a list of monadic values), but since they're kind of different operations, I think it's preferable to have different functions for each.
example = do
a <- ask
modify (+ a)
b <- get
tell [b]
GHC correctly infers: example :: (Num s, MonadState s m, MonadReader s m, MonadWriter [s] m) => m ()I'd be interested in knowing how the order of the combined effect types are resolved in Koka, though. For instance, in `<io, ndet>` the ndet effect will end up conducting a search. How do the (observable) io side effects get woven into that search? In particular, how does Koka know the proper ordering?
In Haskell, you'd be forced to eventually declare an order to your monad stack (although most functions can be agnostic to the particular choice). So in this instance, you would write `LogicT IO a` to indicate that the non-determinism gets resolved first while most functions would be of the form `(MonadIO m, MonadLogic m) => m a` where the parenthesized set of constraints is unordered.
( MonadPlus , MonadState s ) => m a
Given some concrete choices of monads to witness those effects like MaybeT and StateT there are two "orders" we can stack them. MaybeT (StateT s Identity) a
StateT s (MaybeT Identity) a
where Identity forms the "effect free" base of our stack. While functions which operate on types with the order-free signature work for each stack, ultimately the stacks represent different combined effects. In particular, the `StateT s (MaybeT Identity)` stack throws away state if the computation fails while `MaybeT (StateT s Identity)` does not. run1 :: StateT s (MaybeT Identity) a
-> s -> Maybe (s, a)
-- failure captures the result state
run2 :: MaybeT (StateT s Identity) a
-> s -> (s, Maybe a)
-- result state always produced, failure captured
-- only in the result value
So, ultimately the semantics of your effect set depend upon the ordering of your effects. At least in Haskell. I'd be interested to see how Koka handles it.If it's anything like purescript's extensible records effect system then there are "handlers" which determine effect order/priority (see http://www.purescript.org/posts/Eff-Monad/)
?> Nil.head
Runtime Error: List instance has no field 'head'
Such runtime errors could easily be avoided by making head/tail return Maybe, and using Nothing in the case of Nil. This hardly complicates code, but it provides the correct behavior, particularly when it comes to doing "records" properly. The Mars docs give an example of how I consider copying Haskell's semantics directly without improving them leads to flawed code. type Foo:
X(u :: Num, v :: Num)
Y(v :: Num)
There's nothing wrong with `v` here, as it is total, but you can easily introduce a runtime error by using `u` incorrectly. ?> x = Y(1)
?> x.u
Runtime Error: Foo instance has no field 'u'
This is why using records in combination with sum types in Haskell is widely considered a bad idea. It's a misfeature as far as I'm concerned. If given the chance to redo Haskell's awful record system, I'd fix it.The problem can be trivially avoided by making `u` return `Maybe Num` in this example, where it returns Nothing for Y and `Just 1` for X. Ideally, the compiler should check if field names are total over the constructors - if not, it should automatically return an optional type. If the fields are total there's no need. I doubt there would be any noticeable performance cost to moving the error to compile time - particularly because you don't actually need to use it in internal implementations of map/fold and such - you could have an internal function which mimics the current behavior, but don't expose it from the module.
Interestingly, if you use some of the Template Haskell derivations for lenses you get this record-of-sum optionality resolved at least partly.
You make an interesting point. I haven't decided if I agree with you, but here are my thoughts.
If I understand you, you're saying that x.v should have type Num, but x.u should have type Maybe(Num) (because u is not totally covered). That sounds like a very bad thing, because it means that if I were to change the type so that u is covered, by changing Y to:
Y(u :: Num, v :: Num)
then suddenly everywhere I refer to x.u would break, because x.u would now have type Num, not Maybe(Num).
If I was to go for the "safe" option, it would have to be that all fields return a Maybe no matter what (and I think that would make fields quite unpleasant to use).
I think a better plan (which I've designed but didn't implement -- and I realise that now executing this plan would require a backwards incompatible version of the language) would be to do a simple static analysis of switch statements which records exactly which constructors a variable might have at any given program point, and make it a compile-time error to access a field of a variable unless it is provable that the variable has that field.
So in general, x.u would be a compile error. But this code would be legal, and never generate a runtime error:
switch x: case X: whatever(x.u)
You could also do error checking up front to avoid nesting your code too much:
switch x: case Y: error("blah") # Guaranteed to have a 'u'. whatever(x.u)
(For the record, I actually didn't blindly copy Haskell's semantics in this instance; I came up with this scheme myself and then only later discovered that Haskell had the exact same scheme!)
data Foo = X { u :: Int, v :: Int }
| Y { v :: Int }
bar :: Foo -> ...
bar (X u v) = ...
bar (Y v) = ...
If "u" were then added to Y, this pattern matching would break - the names of the fields don't matter in this case - the arity does. We just can't add to Y here without going through every pattern match on Foo to fix it.The only case where changing Y is not a "breaking change" is if Y is never exported from the module - one of Haskell's most underused features is the ability to hide constructors and expose custom functions in their place - such functions would easily allow adding to Y (but not change the function y) without breaking the public interface - we need a default value for u though.
module Foo (Foo, x, y) where
data Foo = X Int Int
| Y Int Int
x :: Int -> Int -> Foo
x = X
y :: Int -> Foo
y = Y defaultU
defaultU = 0
u :: Foo -> Maybe Int
u (X a _) = Just a
u _ = Nothing
v :: Foo -> Int
v (X _ b) = b
v (Y b) = b
Just manually expanding what records do, except you have more control over changes to it - if you expect changes, you probably want to do this. The caveat is that you can no longer pattern match over X/Y from outside the module - unless you expose "isX, isY" functions, and use -XViewPatterns or some other extension. I actually prefer manually expanding things out this way because I consider Haskell's record system to be so poor.It's not just a poor record system, but also the idea of encapsulation. To me, exposing constructors is like making the guts of your classes public in an OOP language - allowing any outsider to access fields really leads to tightly coupled code, and makes it difficult to introduce changes like the one you've suggested.
Consider a trivially modified list class where we want to optimise the performance of `length` if it were used frequently. I could define a data type for it with an extra "Int" field to hold the length.
data LList a = Nil | Cons Int a (LList a)
What would I expose from this module? Almost certainly not the constructors, because I wouldn't want a consumer arbitrarily inserting integers, nor should they need to include it in their pattern matches - what I really want is to expose exactly the same "Cons" and "length" functions as the regular list which doesn't contain the head index. cons :: a -> LList a -> LList a
cons a Nil = Cons 1 a Nil
cons a (Cons n h t) = Cons (n + 1) a (Cons h t)
length :: LList a -> Int
length Nil = 0
length (Cons n _ _) = n
This is really what we want to expose, the constructors themselves don't matter - exposing constructors is only even useful in the cases where the structure of the type maps exactly to the operations we want to perform on it from outside the module, and thus we don't need to "hide" anything. It's not all that common, more often than not you want to hide things (OOP wasn't created by accident). Most haskell programmers think like C programmers - in terms of data rather than the operations you perform on the data.If we continue this OOP analogy to the record syntax, we're treating Foo as some object exposing its public fields and some constructor functions.
public class Foo {
public Int u;
public Int v;
private Foo(Int u, Int v) {
this.u = u;
this.v = v;
}
private Foo(Int v) {
this.v = v;
}
public static Foo X(Int u, Int v) {
return new Foo(u, v);
}
public static Foo Y(Int v) {
return new Foo(v);
}
}
Now, if we construct Foo.Y(1), and attempt to access `u`, we get the dreaded "NullReferenceException". In Haskell and Mars we've simply renamed `null` to "Runtime error", but it's effectively the same thing.To me it's a design flaw. Optional obviates the need for null when used properly. I'm not sure what would be the best way to go about it - whether my previous suggestion, or making all fields Maybe, restricting records to types with only one constructor, or just do away with records altogether, because they're ultimately unnecessary. Writing out your own constructor functions gives you more flexibility, and having record syntax is not all that useful compared to say, using lenses.
About Mars
...
Mars is an experiment, not a full-featured programming
language. You can use it for playing with the boundaries
between imperative and declarative programming, but we
don't recommend you write real software with it.And I think their caveat is clear: they consider the language a research project at the moment, and not only do they not have enough confidence in the reliability for real-world use, they probably don't want the implied responsibility.
Yeah, it basically means a few things:
1. This is a research project. It doesn't have any support and if there are future versions, they might change the language in backwards-incompatible ways (but I will generally follow proper practices and bump the major version number in that case).
2. I am reaching the end of my PhD and I don't anticipate spending a lot of, if any, time working on Mars.
3. The language as it currently stands isn't very palatable to use for real-world software, basically because it is missing a lot of features. I've compiled a brief list here: http://mars-lang.appspot.com/docs/faq.html#what-features-is-...
I think if you were to play with it a bit, you would start to realise how primitive the language actually is ;)
Sorry to disappoint you. Having said that, the source code is fully available and GPL, so there is no technical or legal reason why you can't use it in any way you like.
---
The language looks pretty good. I think for most programmers some kind of vanilla ML would work well.
I believe the 'as' keyword would be more smooth:
def hello(name as String) as String:
var greeting as String
greeting = 'Hello '+name
return greeting def hello(String name) as String:
String greeting
greeting = 'Hello '+name
return greeting
which is how a lot of other languages are doing it. On the other hand, maybe I just don't like writing syntax that doesn't add any meaning :p greeting as String
or even greeting :: String
Either is fine, though my preference would then go to the latter option, since it'll be faster when scanning.A good example of this case would be Java (like it or not):
String Hello(String name) {
String greeting;
greeting = "hello " + name;
return greeting;
}
Say of the braces and the semicolons what you will, but the core syntax is unambiguous and doesn't have a single word that doesn't have semantic meaning.Using a keyword to introduce entity definitions (variables, functions, packages, etc.) gives the language more space to grow in syntax without worrying about such ambiguities later. You can construct a grammar without any ambiguities that does not use such keywords - such as with your example of Java - but you're going to have to keep this pitfall in mind anytime you change the grammar.
It's possible to debate the positive side too, but I do agree that constructing a language with a verboser non-semantic grammar is easier.
LISTEN TO ME VERY CAREFULLY hello
I NEED YOUR CLOTHES YOUR BOOTS AND YOUR MOTORCYCLE name
GET TO THE CHOPPER greeting
. . .
ENOUGH TALK
GIVE THESE PEOPLE AIR
I'LL BE BACK greeting
HASTA LA VISTA, BABY
(I have a truly marvelous ArnoldC string library (with string concatenation), which this margin is too narrow to contain.)Of course, the comparison may not be very illuminating given that all ArnoldC variables are Int16s.
You are as brave as the sum of a fat little stuffed misused dusty old rotten codpiece and a beautiful fair warm peaceful sunny summer's day
is pure art. def hello(name : String) -> io String:
[1] http://legacy.python.org/dev/peps/pep-3107/ def hello(name : string) : io String
The only reason they didn't do that is because colon also means block.... Function hello(ByVal name As String) As String
Dim greeting As String
greeting = "Hello" + name
Return greeting
End FunctionAt university, many of my fellow students wrote their own LISP, to get started with it.
I wrote a few DSLs in XML, which is rather easy too.
With this knowledge it is easier to learn new languages :)
If it's a "deep" concept, I'd pick a simple representation that I can implement without much cognitive overhead (a basic Lispish or Forthish stacks-and-registers syntax is usually something I can spin up in a few minutes) and then start layering my concepts in.
Some of the things you think will be awesome will end up just being wrong or inefficient, but you'll cut the loop down really quickly.
It makes it really easy to compare your hypotheses. If you want to factor them into a more conventional syntax model (c, python, haskell, etc.) you can always learn that along the way.
In fact, if you want to get really simple about it, write the code you would want to write and hand interpret it. You can get really far with simple pre-processors written in whatever text or AST manipulation environment with which you feel comfortable. (I'm notorious for being lazy and writing 50-pass compilers, just writing 10 to 200 line features and chaining them, all in a ghetto RPN.)
It's so much fun!
Are Google that measly with the bandwidth allocations?