adamsol / Pyxell

Multi-paradigm programming language compiled to C++, written in Python.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Types first?

skaller opened this issue · comments

I thought you were against this:

Float x

Why not use he universally accepted correct way to do this, namely

var x : Float

You can leave out the var if you want, but you HAVE to write the separator. You could use the reverse order too, but the separator is mandatory. There's a good example of why your syntax is untenable in your own examples:

A -> B f

That's one of two things from eevee's article that's I'm not sure about (the second one being hyphens in identifiers). I like how function definitions look, and they should be consistent with variable declarations. Why do you think a separator is mandatory? In all examples the types are written without spaces, and that's the style I would like to keep, so it's clear where the type ends. Would it help if spaces in type names were completely forbidden by the grammar?

Yes, function definitions should also be written with colons like:

func f(x:Int) (y:Float,z:Q) (f:A->B): Complex def

This is standard mathematical syntax and also what is used in ML.

There are two simple rules:

  1. The language must be specified by an unambiguous grammar
    so it is context free and produces unique parses

  2. You may not use capitalisation as a distinction because
    this conflicts with I18N considerations (How do you capitalise
    chinese or arabic symbols?)

In general the type and expression languages are distinct so they have
to be universally separated with punctuation or you cannot mix them together.
This is often needed, for example, for casts/coercions/constructors.

When you have indentation/line ends as punctuation it is usually translated into
free form explicit with indent/dedent/endline symbols added so it can be parsed
by a conventional stream parser. I guess you're doing that (Python's parser does,
don't know about Haskell).

I would add some rules too: () should be used for grouping only.
So it cannot be used for function applications.

In expressions <> should be for less and greater so it cannot be used
for indexed types.

Felix uses [] for indexed types, but you're using them for lists.
So you really have a serious problem, you've run out of brackets!

I've created a branch with the new syntax for type annotations. Even though it's slightly more verbose (with the additional colons), I like it, especially that variable declarations and assignments are now more consistent, as the variable name is always at the beginning. I'll probably merge it for the next version.

This also gave me the idea to implement type inference for default function arguments (and class fields), like in Nim: https://nim-lang.org/docs/tut1.html#procedures-default-values.

As for the function call syntax, I think it would be unexpected for an imperative language to have a syntax like this. It would also cause problems with calling functions without arguments. Since I want Pyxell to be familiar for most programmers, I'm going to keep the current syntax, which is undeniably the most popular one: http://rigaux.org/language-study/syntax-across-languages.html#FnctnFnctCall.

The syntax for generic types is certainly a problem, that's one of the reasons I'm postponing this feature. Maybe using curly brackets instead of angle brackets would be enough to keep the grammar unambiguous. Or maybe I should add new keyword for object construction.

The most popular language is Javascript, it uses the syntax most familiar: var x = 1;, typescript is typed Javascript, it uses var x: int = 1; think, as does ML and Felix. Programmers won't be scared of this IMHO, programmers are smart.

The function call syntax in ML and Felix is harder to adapt to: sin x without needing parens. The real issue here is consistency: parens should be used for grouping only. Felix says f x is equivalent to x.f universally, the latter is easy to comprehend. It also means "method calls" and "function calls" are the same thing. You really want that as a way to eliminate special handling of methods. C++ is currently trying to do that, it is called "unified call syntax". They're failing.

You have to design a language from the ground up to be consistent, this is more important than what programmers are familiar with: programmers are familiar with rubbish, that doesn't make rubbish good. In Felix you can still write

f(x)

On reason that operator whitespace is required in ML is the same as Haskell: the heavy use of curried functions you can write code like:

add 1 2

instead of rubbish like

(add (1))(2)

So if you're planning on providing Higher Order Functions (functions as values, closures), you really cannot use parens for function calls, you have to use operator whitespace otherwise your code gets littered with parens. Unfortunately this makes error detection very hard (because any missing punctuation is always grammatically correct, every sequence of symbols is just a series of function applications).

Felix has "generic" functions. This is NOT polymorphism but a kind of inference:

fun f (x) => x + x;
var y = f (42); // 84

The way this works if that the parser desugars the function to

fun f(x:GENERIC) => x + x;

Now when we do a lookup we must find only one f, with an argument of 'type' GENERIC. It's not really a type. So then what happens is the type of the argument is calculated. It's int in the example because 42 has type int. Felix then copies the function replacing GENERIC with int:

fun f_int(x:int) => x + x;

and calls f_int(42). This is done before binding. Now it does binding, that is, lookup, and the call is well typed. The function body may or may not compile. This is a cheat. In Felix the lookup for the body of the function is done in the context of the function definition NOT the context of the function call, which prevents hijacking. This is unlike C++, where the context of the call can also be used for dependent names (that is, names such as + in the example which require lookup based on the generic type).

Real programmers never use these generics in serious code. But they're ok for beginners.

As you know, Felix uses [] instead of <> for generics so it can be parsed with a parser tool. There are alternatives. One I tried was to use <> but then require less than to be written .< with a dot. Another option is generics like

vector.<int>

which uses the dot. A lot depends on your lookup rules, that is, the structure of your symbol tables. Felix base structure is a single hash table, most languages use multiple lists (one for types, one for variable names, etc). So in Felix, if you see int and do a lookup you can determine it is type. In other languages the parser has to know it is a type, so you can choose which list to do the lookup in. This could be significant when deciding if < is a comparison or a generic argument of a function name.

So the Nim you liked gives default arguments. Felix allows both default arguments and also named arguments as well as overloading. This is not really related to type inference. I can tell you the algorithm. To make it work, however, you need record types. Here you see again why you need consistency. In Felix:

proc doit(x:int, y:double, z:string) { ... }
doit (z="Hello", y=42.3,x=1);

works. The argument there is a record value. The order of fields in a record is arbitrary. Felix just sorts them alphabetically internally to get a unique representation. A function can accept either a tuple or a record with field names the same as the parameter names. Note that to make this work, functions must require exactly one argument. It can be a tuple, which simulates multiple arguments, but it is a simulation only:

var x = 1,42.3,"Hello";
doit x; 

This works too. A lot depends how you bind applications. If you have overloading, you have to bind the argument first, then you lookup the function name and get back a set of candidates. Then you bind the candidate domain types, and try to match the argument type with the function domain type, usually using a variant of unification, to select which function is involved. Note if you do this you can call polymorphic functions without <> or [] suffix since unification sets the type of the type parameters, or at least some of them.

In Felix, function definitions can have default arguments:

fun f (x:int, y:string="Hello");
f (x=1); // y is set to "Hello"

I think from memory if you want to use the defaults you have to either use record syntax for the call, as shown, or default only the trailing arguments if you call with a tuple. Not sure, I forget. The algorithm is messy but not that difficult. The main problem is that this fudging only works easily after you have selected a function from the overload set. Otherwise you really have problems, because even though you can try out the defaulting on every candidate, if two match you now need a disambiguation rule.

Overloading really makes things very hard. If you add parametric polymorphism and subtyping the lookup algorithm becomes extremely complex. Its even harder in Felix because it uses "setwise" lookup (symbol tables are hash table that contain ALL the symbols in a scope, including ones "not defined yet"). Even worse, Felix also has type constraints. It took well over a year to get the lookup rules to work ... actually I lie, I just found a bug, 10 years later.

The problem is if you don't have overloading you cannot have generic + and everyone expects that. In Ocaml + only adds integers, you have to use .+ to add floats and ^ to add strings and @ to add lists. Easy to run out of infix operators.

You have some really tough language design decisions to make. If you want overloading and generics .. you will probably have to rewrite the compiler in a real language, there's no way Python can do it.

Felix uses Haskell style type classes to implement "generic" operator +. This means that it is first considered polymorphic, and then later in a second phase of lookup, the correct + is chosen from available instances. This is done by "overloading" of the instances, with a different algorithm to ordinary overloading, BUT both these algorithms use the unification algorithms as their core way to do matching.

It seems like you're again trying to redesign my language without even having read the whole documentation. And you have misunderstood what I said.

Pyxell does have default function arguments. It doesn't currently have type inference for them, which means that you need to provide the type, even when you set a default value of some known type, like arg: Int = 0. It's not completely consistent with how variable declarations work, so I'm thinking of changing it.

Pyxell does also have named arguments. No special record types needed.

Pyxell does have generic functions, both as full definitions and as lambdas without any type annotations (working similarly to what you described). It doesn't have generic classes, and this is where the syntax ambiguity problem arises, because when an object is constructed, the type name may be in the middle of an expression.

So if you're planning on providing Higher Order Functions (functions as values, closures), you really cannot use parens for function calls, you have to use operator whitespace otherwise your code gets littered with parens.

Pyxell does already have higher order functions and closures. The syntax for function calls has nothing to do with it. And I think the current syntax requires in practice less parentheses than the other approach, since expressions are used as arguments more often than bare variables. That's my experience after using Ocaml and Haskell for a while.

Felix says f x is equivalent to x.f universally, the latter is easy to comprehend. It also means "method calls" and "function calls" are the same thing. You really want that as a way to eliminate special handling of methods.

I don't agree. That actually introduces an inconsistency and makes the programmer wonder which syntax to use. My goal is to avoid many ways to do the same thing.

The problem is if you don't have overloading you cannot have generic + and everyone expects that. In Ocaml + only adds integers, you have to use .+ to add floats and ^ to add strings and @ to add lists. Easy to run out of infix operators.

Pyxell doesn't have operator overloading for custom classes yet, but the operators like + are internally overloaded for the built-in types and I can't see any problems with it.

you will probably have to rewrite the compiler in a real language, there's no way Python can do it.

I have deliberately moved away from Haskell to have the more natural expressiveness of Python. And the real compilation is actually done by C++. Please, keep your opinion about "real" and "junk" languages for yourself.

Let's not continue this dicussion, as it doesn't lead anywhere. This issue was about changing the syntax for typing, which I did, since it's more consistent with variable assignments and also with other languages.

If you have an idea for another change and some relevant arguments for it, please create a new issue. But first, read the documentation and try to understand Pyxell's assumptions. I don't want to answer again how Pyxell is not Felix or Ocaml, and I don't want to repeat things that are already described in the documentation.

Ah, I agree I misunderstood what you said when I looked at the Nim link you posted, sorry.

I have read the docs, at least up to classes which I don't care much about since OO doesn't really work. Generic functions are not the same as polymorphic ones. As you say polymorphic classes always need a type parameter, functions may or may not depending on whether you can deduce the type variable specialisations or not.

I'm not trying to redesign anything, that's your job. At most I can point out the limitations of certain approaches. It is actually quite hard to figure out Pyxell's assumptions. The docs give examples of concrete syntax, not the algebra behind them that actually matters. The principles that are stated are mainly about the concrete syntax: the result seems quite clean. Also you use the same machinery as Felix: translation to C++, which is one reason for the interest. Not many languages choose C++ as a target (most chose C, although these days some are using LLVM bytecode).

Pyxell has real generic functions (https://www.pyxell.org/docs/manual.html#generic-functions), not just polymorphism. However, in my implementation the types are always deduced when calling such a function. In most cases the arguments and the function body have all the type information necessary for compilation.

My assumptions are briefly described in the readme and in the front page of the docs. Most of all, I want Pyxell to be intuitive for programmers coming from other languages. The starting point is Python's syntax and C++'s semantics. The syntax of languages like Ocaml or Haskell is not a good argument for me, since they are too niche. However, many different languages have some interesting ideas, so I'm open for suggestions, as long as the idea fits those basic assumptions. Just please, one thing per issue.

I chose C++ as the backend language to have some nice features, like containers, generators, closures, memory management, etc., practically for free. I used to have a backend in LLVM, but that was too limiting and I didn't want to reinvent the wheel. Unfortunately, C++ has one downside: long compilation times. That's why I'm now thinking of creating also an interpreter for Pyxell, though that would indeed be difficult to do in Python due to the different semantics. The interpreter would probably need to be written in C++ itself.

OK, but my issue at the moment is that I do not understand the semantics in so many cases I don't know where to start. I do not know what you mean by real generics as opposed to polymorphism, since generic is a waffle word with no precise meaning, whereas parametric polymorphism is very precisely understood. I have a personal definition of "generic" and it is vasty inferior to parametric polymorphism semantically. I also use the term polyadic, which is a form of higher order polymorphism (also called functorial polymorphism, it means "independent of the data structre"). As an example, you might say C++ iterators are polyadic, in the sense you can write algorithms that work on a container without knowing the container type. Generic also covers that. However C++ generics are not really polyadic because they're untyped, in fact C++ doesn't really have polymorphism either because to me at least that implies static typing.

I have no idea what the lookup rules are even in simple cases in Pyxell. For example in a function f, can I call a function g that is defined later lexically? The lookup rules in "generic" functions need to be specified. In C++ these rules are what lead to a seriously broken system: templates in C++ have no semantics and cannot be type checked. C++ uses two phase lookup where non-dependent names can be bound in the context of definition, whereas dependent ones are bound in the scope of definition AND the scope of use AND the scope in which a type is defined. The latter is sometimes called Koenig lookup, because the lookup rules were proposed for operators by Andrew Koenig, and then several people, including me, suggested extending that to all dependent names (I invented the name Koenig Lookup). Its called DNL in the Standard I think. These rules are extremely bad. The original Concepts proposal (basically Haskell type classes) might have fixed that by eliminating DNL, and allowing templates to be type checked but it didn't make it through the committee and a weaker, more or less useless version was accepted instead.

I'm explaining this just to point out that lookup rules matter and you haven't specified them. Even the "use" feature isn't specified: is it transitive?

If you use linear scoping rules (you can only refer to what is previously defined) and you do not have separate interface specifications for functions .. then how do you do recursion? I can't raise an issue because whilst the examples clearly indicate syntax, the semantics isn't really specified. I consider writing docs extremely hard and more labour intensive than programming so for sure don't take that the wrong way.

Can you define recursive types? In C you can, just say "struct X*". Can you define recursive polymorphic types? In C++ you STILL cannot do something so utterly basic.

BTW: I actually like what you've done syntactically. Its nice and clean. And syntax matters. But so do semantics. if a language is to be reasonably general purpose it needs good semantics too particularly in respect of major features like polymorphism and subtyping. Memory management is also a very difficult issue.

By generic functions I mean that you can write a function once and it will work with any types the body can be compiled with. This is similar to templates in C++. But Pyxell doesn't use C++'s templates internally, just a new function is compiled for every combination of types the generic is called with.

Alright, some technical details are missing in the docs. That's because it is currently closer to a tutorial than to a full-fledged specification. I've concentrated on describing what is useful for writing standard programs, and assumed that if everything works consistently and similarly to other languages, people will naturally discover and understand the other minor details after playing with the language for some time. That's also why I included the playground: so that anyone can quickly test how a given code behaves. For me, personally, that's the best way of learning a programming language, once you know the syntax. Many of your questions could also be quickly answered that way.

I have no idea what the lookup rules are even in simple cases in Pyxell. For example in a function f, can I call a function g that is defined later lexically? The lookup rules in "generic" functions need to be specified.

There is currently one environment for all variables, including functions, and only one lookup rule: you can use what has been defined earlier. Recursion works in the most obvious way: when a function is being compiled, it's automatically added to the environment for its own body. The same goes for recursive types, this can work since class objects are actually pointers. You can't, however, call a function that's defined later in the code. So mutually recursive functions won't work, and the same goes for classes. This might indeed need clarification in the docs, or, even better, fixing, since in many other languages this works, though it's probably not used everyday.

Even the "use" feature isn't specified: is it transitive?

This feature is far from being finished and can be currently used only for the few built-in modules. Once custom modules are implemented, it probably won't be transitive, not to clutter the namespace, though I'm not sure if there won't be some other mechanism for this.

"By generic functions I mean that you can write a function once and it will work with any types the body can be compiled with. This is similar to templates in C++."

Ok. So, like C++, they have no semantics or types, only their instance do .. and neither is specified until the lookup rules are specified. Then the monomorphic typechecking rules can determine if the instantiation is valid. This also implies they're only callable if their bodies are visible. In particular they lack functional abstraction. OTOH Haskell, Ocaml, and Felix polymorphic functions have types and semantics, can be compiled independently of their use, and don't require any type checking on instances, similarly Ocaml functors, whilst Felix and Haskell type class functions are first treated as if parametric, and then either work or fail after type checking depending on whether suitable instances are available.

The effect is you can reason about Haskell, Ocaml, or Felix polymorphic entities but not C++ of Pyxell ones. IMHO this breaks the Pyxell specification of simplicity, the cornerstone of simplicity being local reasoning. Dynamic typing is even worse, it only fails at run time, and only if you trigger a particular use case: C++ templates are the same I discovered when i wrote slist (a purely functional list in C++ which outperforms Ocaml and Haskell lists).

"But Pyxell doesn't use C++'s templates internally, just a new function is compiled for every combination of types the generic is called with."

Are you sure? This is not so in C++. Why? Because two identical calls can have different implementations because the lookup can depend on the context of the call and therefore the semantics can too. For example if you call f on type T=int, and the body calls h of T, and h of int has a different return type in two calling contexts, the semantics may differ. Indeed the body may compile in one context and not the other because h is not found, which is radical difference in semantics.

This problem cannot arise with parametric polymorphism because if you have a function dependent on a type parameter you have to pass it in to the function, either explicitly as an argument at run time (most flexible) or as part of a package of functions, or, you can pass it in at compile time by say using a type class.

In Felix the generics are fully bound in the context of definition, irrespective of the context of the call. This means they also have no types, and have no semantics, however, the semantics are invariant over calling contexts. They're more definite than C++ generics but still a very weak construction. Its basically a bad kind of overloading. Overloading has similar bad properties. Felix has that too. Ocaml doesn't. Haskell does, but constrained by the type class machinery. But Felix has parametric polymorphism as well, and, also has type classes on top of that.

You might consider that fixing minor warts in C syntax isn't enough and fixing the warts in C++ generics is worth thinking about. Parametric polymorphism is fairly easy to implement (trivial if you have no overloading, a bit harder if you do, and a bit harder again if you have type class like constraints). You do however lose output types without considerable effort. Felix can do them but only with type classes. C++ generic are very powerful, that's the problem with them.

I could try the playground but that misses the point. What you want and what you have at the moment can differ. I cannot tell by experiment, whether its a bug because it fails to implement what you intend, or a design issue because it is what you intend, because you have not documented what you intend. I get the flavour, but the devil is in the details :-)

It's like functions taking one argument, or multiple arguments. With tuples, you can have functions which look like they take several arguments but actually only take one, a tuple. Or similarly using curried higher order functions. Or you can have actual function like things which take multiple arguments. Swift started using tuples and switched to multiple arguments. C, C++ and Rust use multiple arguments. Multiple arguments are grossly inferior because they lead to a combinatorial explosion of higher order operators. In most languages they're not even sound. The way it works if function can only have one argument, end of story. Tuple constructors (infix comma usually) are not functions but functors. So by adding one construction, the pairing functor (or a family of n-tuple constructors) you can simulate multiple arguments by first applying the functor to combine values into a single product, and then apply the function to it.

The other way to do that is to take the function and apply the functor to it first, so now the combination takes multiple values: the result is a functor NOT a function. Its a functor over a product of the type category, for two arguments what in Felix is spelled TYPE * TYPE (Haskell spells the component *) These things are much harder to work with because given such a bifunctor you cannot decouple it into original function and pair functor. This is why C++ is a disaster. Templates are very hard to write that work for all functions, because the number of arguments can vary. For example C++98 had a bind operator but it only worked for a pair of parameters, not for three or more. You can do it now, but it required major extensions to templates (parameter packs and so forth).

So Pyxell has tuples .. but do functions strictly take one argument, which could be a tuple, or do you have multiple arguments, which makes general generic almost impossible?

Are you sure? This is not so in C++. Why? Because two identical calls can have different implementations because the lookup can depend on the context of the call and therefore the semantics can too. For example if you call f on type T=int, and the body calls h of T, and h of int has a different return type in two calling contexts, the semantics may differ. Indeed the body may compile in one context and not the other because h is not found, which is radical difference in semantics.

I don't think this is possible. The environment for every function is fixed at the moment of definition. The only thing that can change between generic function instances are the argument types. Anything else doesn't depend on the calling context. If a generic function has been successfully compiled for T=Int once, the same instance can be used for any other call with T=Int. Just like a standard function can be compiled only once and its implementation doesn't depend on the calling context.

IMHO this breaks the Pyxell specification of simplicity, the cornerstone of simplicity being local reasoning.

You might consider that fixing minor warts in C syntax isn't enough and fixing the warts in C++ generics is worth thinking about.

Actually I like how templates in C++ work in the basics and I consider them very simple (if we limit our thinking to types and forget that templates are actually a Turing-complete language themselves). So I've decided for a similar solution in Pyxell and it works well enough for me. I think that generics with additional restrictions like in other languages are less intuitive, e.g. in C#: https://stackoverflow.com/questions/8122611/c-sharp-adding-two-generic-values.

So Pyxell has tuples .. but do functions strictly take one argument, which could be a tuple, or do you have multiple arguments, which makes general generic almost impossible?

Tuples are a completely independent mechanism. Functions have multiple arguments just like in any language that I know.

Huh? Haskell, Ocaml, and Felix do not have multiple arguments.

Ah, in Haskell and Ocaml a function returns a function that returns another functions etc., right? Easy to forget because of the syntax sugar. But still, I don't see what it has to do with tuples. And I think the current syntax in Pyxell is quite clear that there are multiple arguments. Really, if it worked differently to how it works in the mainstream languages, it would be described in the documentation.

Currying is separate from tuples, correct. Felix does both. Here is tuples:

fun f(x:int, y:int) => x + y;
println$ (1,2);
var z = 1,2;
println$  f (z); // see? one argument

The function f only takes one argument of tuple type int * int. Since z has that type, f can be applied to it. Its a single value in both cases. You can also write HOFs: in long hand and most generally:

fun f(x:int) => fun (y:int) => x + y;
println$ f 1 2;
fun g(x:int) (y:int) => x + y; // sugar for the same as f
println$ g 1 2;

Note that returning a function explicitly in case of f is more general than the sugar, because you can do any calculations before returning the result. If you have tuples OR HOFs multiple argument functions would be madness. Its bad enough that you have to choose (as I never can) between the tuple or curried form when defining a function. Felix prefers tuples because overloading works better so most binary operators like + bind to functions taking a single tuple argument with two components.

There is a relation between these two ways of doing things, tuples and curried functions. It is called the Yoneda Lemma that says the two things are directly related: it's a basic adjunction. One of those things in category theory where the maths is hard for mathematicians but trivial for programmers to understand.

So the syntax is not clear in Pyxell. It is in Felix because I gave an example which could not work if a function was taking two comma separated arguments: the function works fine on a single tuple as well. This is much better IMHO for reasons explained: the simplest algebra says functions have one argument. If you want Pyxell to have the simplest semantics you should implement that, not functions of multiple arguments. When you're binding to C/C++ you can always unpack the tuples. That's what I do. Then, if the argument was a literal tuple, you can optimise it and just pass the separate arguments. But passing a single tuple value will still work:

fun add : int * int -> int = "$1+$2";

This function is binding Felix function add to C addition operator, $1 means the first component of the tuple argument. This function can be called with a single tuple valued argument. The compiler unpacks it.

I believe the syntax is clear for anyone who programs in the mainstream imperative languages on a daily basis. Tuples have their uses, but simulating multiple argument functions is not one of them. It would not simplify things, quite the reverse. And I really don't see what you're trying to achieve by constantly undermining every aspect of Pyxell, so please, let's stop this discussion. It is not Haskell, Ocaml, or Felix.

I'm not undermining it. All I said was you didn't document things. Actually I'm trying to help stop YOU undermining it since I've been designing languages for 30 years and that includes 10 as a member of the C++ committee. So I know some of the pitfalls.