An English like programming language in its earliest stages

This is my first self-designed language and the first try/version of it that I am willing to upload. This is not in anyway finished and currently is not even close to be Turing-complete. It is also completely undocumented aside from this post.

So this language (I am still missing a name) consists of two parts:

  • The feature definition
  • The actual code

The feature definition defines the syntax that can be used in the actual code on a token basis. Currently these definitions still have to happen inside of python, but will be moved from there sooner or later. These feature definitions have the following syntax:

feature_header: "[" (name|typed_parameter)+ "]" "->" name
typed_parameter: "{" name ":" name "}"
name: <The normal c alphanumeric name without unicode (may change)>

This allows as example for the following header:

[print {data: str}] -> void

Which indicates a feature that takes a str as parameter and returns void (ie, is a statement). This can the be used as follows in the actual code:

print "Hello World"

But you can also define a replace-in-string feature:

[replace {old: str} with {new: str} in {target: str}] -> str

This can then be used like this:

print replace "," with " " in "a,b"

Reads almost like an English sentence! This would print out:

a b

But what if you want to have a different sentence structure or tell the program how often to do the replacement? Then you give more then one header per definition:

[replace {old: str} with {new: str} in {target: str}] -> str
[in {target: str} replace {old: str} with {new: str}] -> str
[replace {old: str} with {new: str} in {target: str} {count: number} times] -> str
[in {target: str} replace {old: str} with {new: str} {count: number} times] -> str

This then allows for the following variations:

print replace "," with " " in "a,b,c,d"
print in "a,b,c,d" replace "," with " "
print replace "," with " " in "a,b,c,d" 2 times
print in "a,b,c,d" replace "," with " " 2 times

This prints:

a b c d
a b c d
a b c,d
a b c,d

All of this should allow for a very interesting language, that, if written correctly, is very verbose, but very easy to read. What are your opinions on this? (Ideas for a name are welcome)

A version requiring python3.7 is on github

Posted Oct 28, 2018, 8:06 PM UTC · edited

1122
tripl3dogdareServal•

This looks pretty cool! I look forward to seeing where it goes.

3
thenameipick•

When you see the word "in", how do you know if that word is the start of a new feature vs a word in the middle of a feature? I see several solutions (pick one or all):

  1. Upon ambiguity, maintain a list of all possible parsings. Continue parsing until all but one remains. (There is no guarantee that this will happen, and can be expensive, so I like this one the least)
  2. Require parenthesis in these cases. This makes your code less ambiguous, but makes your code less like English.
  3. Have the language style guidelines indicate that features start with verbs. This rules out some of the examples you listed out above, but reduces the number of times ambiguous parsing is an issue.

​

Finally, I think that *individual* features are readable, but that when combining them, it can easily lose its readability.

​

For example, `print "cat"` is readable, `replace "cat" with "dog" in "cats"` is readable, but `print replace "cat" with "dog" in "cats"` isn't. We're using english words, but the grammar basically breaks down and we are back to reading it like we would any other language.

3
MegaIngOP•

I choose option one. Yes, the parsing is very expensive, but I am against option 2 because that is the core idea of this language. The parsing library I am using is self written and with the main goal of simple creation of Parser at run-time, so the performance is already very bad. I may at some point change that if I find something better, but I will properly never use option 2 or 3.

For example, print "cat" is readable, replace "cat" with "dog" in "cats" is readable, but print replace "cat" with "dog" in "cats" isn't. We're using english words, but the grammar basically breaks down and we are back to reading it like we would any other language.

This should be fixed with a different syntax option for replace feature:

[{target: str} where {old: str} is replaced with {new: str}] -> str

Then the example could be rewritten as:

print "cats" where "cat" is replaced with "dog"

Which reads way more like an English sentence the beforehand.

My example in the post was not that good, I know. But this improvement only came to my mind after your comment.

1
thenameipick•

Ok. I *do* think that for small projects and such, #1 shouldn't cause you issues, but I guarantee it will cause issues in any production-level environment.

​

In regards to the readability changes, you are right, it is entirely possible to write them differently to make the English flow better. However, it's a constant (and a losing) battle, but one you can certainly fight if you want to.

As a real life example, let's assume I was writing a SQL generator:

print QUERY where BASE_SELECT_STATEMENT where "*" is replaced with "columnName" is replaced with "columnName2"

​

The above code would take a sql query and changes the column to be returned.

I *do* think that readability is a fight to fight, but identifying readable syntax takes careful consideration and often multiple iterations.

2
MegaIngOP•

This language is made for someone who wants readable code. Yes, you can easily make very unreadable code with this, but even in other languages this may look weird:

print(QUERY.replace(BASE_SELECT_STATEMENT.replace("*", "columnName"), "columnName2"))

I, to be honest, can't even imagine why somebody would want to do that. Nested replace will probably never be easily readable. But still this language is probably an improvement because you can change the syntax if it annoys you or is unreadable. This will be especially the case if I implement a simpler way to add features than to write a python function.

1
thenameipick•

I agree, nested replacement isn't a common use case, but nested operations in general are. I'm simply using the only syntax I have to work with :). Regardless, I think that my point stands: nested operations are difficult to get readable. If you only allowed a single operation per line, then you'd get super readable (but also verbose) code.

​

In regards to "simpler way to add features than to write a python function", I'm not sure what you mean. If you are talking about renaming features (or their Python counterpart, functions), I think python has it the simplest: `myreplace = replace`. It's entirely possible to match that, but I'm not sure how you could make it simpler.

If you're simply talking about a simpler way to write functions from scratch, that's pretty hard to measure, so it really just comes down to the final product. If this is what you are talking about, perhaps an example?

1
MegaIngOP•

I do think that readability is a fight to fight, but identifying readable syntax takes careful consideration and often multiple iterations.

That is exactly what this language is for. You will at some point in the future be capable to very easy change syntax or add new syntax to fight exactly that fight. This would make refactoring your many activity which may be bad, but at the end you will have something that can be easily understood even by a not programmer. If you take enough care with the design of the features, you may even be capable to create a language that can be written with no programming background. But this language could potentially be compiled down to C or below which would make this a very powerful tool.

1
thenameipick•

This makes a couple of assumptions:

  1. You have access to all code that is using your features
  2. Being readable by a non-programmer is more valuable that the time you spend refactoring

#1 is definitely false for any real, production-level language. So, when you want to refactor something post-release, it will have to be a breaking change. (As well as any package that wants to refactor for readability).

I'm not convinced that #2 is true, but that's a matter of opinion and target audience. I could definitely see a market of products that want to allow the average consumer to program them (and want a language, and not a form).

1
Barrucadu•

To be honest, I'm not sure I'd agree that it's very easy to read. It seems to me like you'd need to know the definition of every feature to have any hope of knowing what a program means.

For instance, my initial reading of print replace "," with " " in "a,b,c,d" 2 times was as meaning to do the print twice, not to do the replace twice.

2
MegaIngOP•

That is a problem I did not have, because I designed it, but that makes sense. Would

print "a,b,c,d" where "," is replaced with " " 2 times

be better? In contrast one could define a repeating print like this:

print 2 times "a,b,c,d" where "," is replaced with " "

I don't know if that is any better and that is a problem I did not thought about. It may be unambiguous for the compiler, but ambiguous for the human compiler. Do you have a solution that does not involve parentheses?

1
Carbonite1•

People are pointing out a lot of very valid criticisms here, which is great, but I also wanted to say that I love the idea even if it is hard to get the kinks worked out, and trying crazy stuff like this is how real innovation happens -- cool to watch, so thanks for sharing :)

2
bruciferNomsu•

I've been working on a language that works kinda like this ("mixfix" syntax), and there have been some other languages that work this way, like AppleScript and Smalltalk. Here's some insights I've had after working on (and in) the language for a considerable amount of time:

  • It's good to have some way to disambiguate parsings. Static compilation and strong typing is one option. If you can determine at compile-time the names of every function and variable name, you can brute force check all parsings and eliminate ones that don't typecheck. So, e.g. print my_nums sorted can only satisfy type-correctness if it's parsed as print(sorted(my_nums)). However, if your language is dynamic, you could define a function called [my_nums {xs: list}] -> xs[0] and a variable called sorted, so it could parse as print(my_nums(sorted)). My language is dynamic, and it solves this by requiring you to use parentheses for grouping, and having variable names use % as a prefix, so it would be print (%my_nums sorted)
  • You have to make sacrifices for ergonomics. For me, that meant having %x = 5 instead of set %x to 5, because if something commonplace is a pain in the neck, your language will be miserable to use.
  • If you want your language to read like grammatical english, you should have a convention of using noun phrases for pure functions that return values, true/false statements for pure functions that return booleans, declarative sentences for declarations, and imperative sentences for actions that modify things. Ideally, you should minimize the use of functions that both do side effects and return meaningful values. Some examples:
    • print "hello" is an action with a side effect, so it makes sense to use an imperative sentence.
    • Instead of replace "," with " " in "a,b,c", you should use "a,b,c" with "," replaced with " ", since "replace" is a description of how to get the value, not what the value is. Alternatively, replace "," with " " in my_string could indicate a function that performs an in-place modification of my_string and does not return a value.
    • In Python, the difference between noun phrases and imperative sentences can be seen with sorted(x) (returns a new value) vs. x.sort() (modifies in-place).
    • For a boolean function, something like list has item works well when used in a conditional expression like if my_nums has 5 ... or while my_buffer is not empty...
  • One thing I grew to appreciate with experience is how much less guessable english-like APIs are, compared to less verbose APIs. For example, %str with "," replaced by " " might equally well be %str with "," replaced with " ", but replace(%str, ",", " ") has only one token/word to remember. Or %x has %y could be %y is in %x or %x contains %y or %x has member %y, and so on. It helps to have really strict conventions, and to prefer function names with fewer words and fewer plausible alternatives. That way, you can figure out a function's name by asking "what's the conventional way to express this, and among the acceptable ways, which is the shortest?"
2
MegaIngOP•

It's good to have some way to disambiguate parsings. Static compilation and strong typing is one option.

That is also the way I am currently doing it. What you mention with ambiguity, I currently enforce that a program can only be interpreted one way. If there is unambiguoty, the parsing will fail.

You have to make sacrifices for ergonomics.

This proposal doesn't, this is a flaw I am aware of. This also prevents the user to actually place correct symbols inside of the program, like dots or commas.

If you want your language to read like grammatical english, you should have a convention of using noun phrases for pure functions that return values, true/false statements for pure functions that return booleans, declarative sentences for declarations, and imperative sentences for actions that modify things. Ideally, you should minimize the use of functions that both do side effects and return meaningful values.

This is just because of really bad example choosing. I did not thing this formal about the rules, but this is exactly how it should be done. In some weired way, this is supposed to be a pure functional language with no site effects at all.

One thing I grew to appreciate with experience is how much less guessable english-like APIs are, compared to less verbose APIs.

To this I say: If two things are grammatically correct, they should both work. This means %k is in %l, %l has %k, and %l contains %k should all be ok and implemented.

1
bruciferNomsu•

To this I say: If two things are grammatically correct, they should both work. This means %k is in %l, %l has %k, and %l contains %k should all be ok and implemented.

That was my initial approach too, but I found myself putting in a lot of work playing catch-up (adding synonyms as I thought of them). After time, I found it was also more effort for me as a user to try to decide which version to use (and wonder whether or not it was implemented), it created more inconsistent style, and it made my code harder to search. As I settled on having a more consistent, predictable style, it made all those things easier, while still having the code read very naturally. The searchability issue shouldn't be taken lightly either–there are lots of tools, like a text editor's search, that aren't smart enough to understand that "contains" and "has" mean the same thing. Encouraging different styles makes working with those tools harder.

I don't mean to say you have to do it the way I did, but just be aware that API obviousness can be a problem.

1
brogam•

I think you need some way of dividing function calls.. or just show argument lists with braces

1
MegaIngOP•

There is no concept of a function call. And I removed brackets in the actual code so that it becomes more english like.

1
brogam•

then what do you call 'replace' or 'print'? Oh and after you answer please reflect on whether your answer about no function calls is still meaningful.

1
MegaIngOP•

Sorry, that comment was written in a hurry. I don't call them functions, but features because for me a function call has always a similar syntax <function>(<arguments). This is the point that doesn't exist in this language, there is not single syntax to 'call a function' which is the reason why I named it feature and not function, event though it boils down to the same concept. This is also the reason why there isn't a 'way of dividing function calls'. That wouldn't make sense with an arbitrary syntax. Maybe it isn't obvious from the examples, but you can any combination as a feature header:

[{data: number}] -> str

This would represent a feature to turn numbers into strings. Would you still call that a function call? Or the following:

[true] -> bool
[false] -> bool

These certainly aren't function anymore, are they? But they are features. This should allow for a very free design of a program, which should still be at least somewhat readable.

1
Edhebi•

They are still functions. Specifically, the last ones are function with no arguments that return bools, which is pretty much how constants are written in functional programming (Haskell, ML family, F#, ...). Functions are mostly of the form foo(bar), but foo bar, bar foo, or (foo bar) are just as valid.

1