pyos / dg

A programming language for the CPython VM.
http://pyos.github.io/dg/
MIT License
577 stars 20 forks source link

exceptions in if-cases should be false #15

Open maweki opened 8 years ago

maweki commented 8 years ago

tuple unpacking in python is very useful. One can unpack a list into head, *tail = somelist and it looks a bit like haskell's case x' of x:xs. If exceptions in the if-cases were considered as false instead of being thrown, we could have a function that looked like this

own_list_to_str = l' -> if
  ([] = l) => "END"
  ([l, *ls] = l') => (str l) + " " + (own_list_to_str ls)
  otherwise => 'wrong type'

one could throw an exception if none of the cases fits.

This would make the language a bit more useful which might be unwanted. But it would also be a bit like pattern-matching but not quite, throwing haskell-people off even more.

If this were to work, having tuple unpacking in the case without the brackets would be even nicer. But one thing at a time.

pyos commented 8 years ago

Interesting idea, although a little hard to implement because failed assignments throw rather generic ValueErrors and TypeErrors -- simply making (a = b) => c equivalent to

(except
  err => a = b
  err :: ValueError or err :: TypeError => False
  err is None => True) => c

would catch a lot more than needed.

As for making parentheses optional, that would also affect how a = b => c is parsed outside of ifs, so not feasible. (=> being an operator on its own is a thing because if x => followed by an indented block behaves weirdly. Frankly, if, except, and => are all poorly implemented and need to be redesigned.)

maweki commented 8 years ago

Well, I think this would be correct.

if
  a[1] => something
  otherwise => False

I would be fine with going to the otherwise-case instead of getting an error if the element has not getitem or if we get a key-error. There is no nice way to catch an exception in the condition of the if-construct anyways.

So as I see it, any exception in the condition-clauses should just make the condition evaluate to false. I see this could be a bit of an issue since if could return None if none of the cases matches so one idea to fix that would be to throwing a "non-exhaustive patterns"-like error that could be nicely catched with an outside except.

So as I see it, any condition => block in the if-construct should be equivalent to

(except
  err => condition
  err :: Exception => False
  err is None => True) => block
pyos commented 8 years ago

Not sure hiding all exceptions in conditions is a good idea. A condition is an arbitrary expression, possibly with side effects, etc. Most likely cause of an exception is not a failed pattern match, and simply ignoring an error is probably not the correct way of handling it.

Anyway, I've special-cased (a = b) => c where a = b is an unpacking assignment, so the pattern-matching aspect of your proposal should work, at least. (It works by checking that b is an iterable of correct length before performing the actual assignment, so no exceptions other than those during UNPACK_SEQUENCE are caught.)

maweki commented 8 years ago

ignoring an error is probably not the correct way of handling it

I don't think I agree.

  1. When an exception happens, none of the cases is executed, not even a following "true-ish" one.
  2. There is no good way to catch an exception within a single case
  3. EAFP "This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements." (https://docs.python.org/3.5/glossary.html#term-eafp)

Given (3) it is a common pattern to just get an attribute or an item or unpack something and catch an exception. But given (2) there is no good way to catch that exception within multiple conditions. The otherwise keyword already is (at least semantically) a fallback if all condition-statements were "false-ish" but given (1) is still not executed. Also, the statement in the condition should (usually) not have side-effects given the pythonic pattern of either having side-effects or returning a value but not both.

Anyways, thank you for the unpacking-fix. It's greatly appreciated. :)

maweki commented 8 years ago

What I forgot to ask: does the fix work with an arbitrary nesting (it does in python)?

([x,*xs],[y, *ys]) = (x', y')
pyos commented 8 years ago

Ah. Damn. No, it does not. Gonna have to fix that, I guess...

maweki commented 8 years ago

This would be my test-cases, reimplementing zip and a list-constructor:

zip' = x' y' -> if
  ([x, *xs], [y, *ys] = x', y') => yield (x, y)
                                   yield from $ zip' xs ys
  otherwise => yield from ()

mylist = x' -> if
  ([] = x') => []
  ([x, *xs] = x') => [x] + mylist xs

print $ list $ zip' ( range 3 ) ( range 7 )
print $ mylist $ range 6

For the first one I get ValueError: need more than 0 values to unpack and for the latter ValueError: too many values to unpack (expected 0). The exception is not caught

pyos commented 8 years ago

The first test isn't actually valid -- you have a call to yield followed by an indented block, so the statement is equivalent to yield (x, y) (yield from $ zip' xs ys). A correct version would be

zip' = x' y' -> if
  ([x, *xs], [y, *ys] = x', y') =>
    yield (x, y)
    yield from $ zip' xs ys
  otherwise => yield from ()

Other than that, both seem to work on my machine. However, Arch Linux ships Python 3.5 in its repositories, and I was too lazy to compile 3.4 and 3.6, so only the cpython-35 bundle actually contains the changes. Try running python3 -m dg -b first if you're using a different version.

maweki commented 8 years ago

Thank you, python3 -m dg -b fixed the issue. And also thank you for the fixed test-case.

I will present dogelang at a local Haskell conference and "pattern matching but not quite" is a fine addition to the talk :)