4 ms·
The fun thing is that this sounds like "just" a more type-safe version of the way DSLs are usually implemented in imperative languages, particularly scripting l
by comex 2y ago
The fun thing is that this sounds like "just" a more type-safe version of the way DSLs are usually implemented in imperative languages, particularly scripting languages.
In Python, if I have:
def foo(x):
return x**2 - 1
I can plug in a concrete argument:
>>> foo(2)
3
But I can also plug in abstract values provided by some library where all operators are overloaded to return another AST node. For instance:
>>> from z3 import *
>>> expr = foo(Int('x'))
>>> expr
x**2 - 1
>>> type(expr)
<class 'z3.z3.ArithRef'>
And then I can perform abstract operations on the resulting AST node, like solving for x:
>>> solve(expr == 0)
[x = -1]
Of course this is completely non-type-safe. And without purity, there's no guarantee that `foo(Int('x'))` is truly equivalent to running foo on an abstract value `x`. If there's an `if` statement in there, the condition will either pass or not, and the returned AST node will only include that control flow path. The DSL in question tries to block that by preventing AST nodes from being converted to boolean, but it's not perfect. Haskell has an advantage there.
On the other hand, for the situation at the end of the post, where the author wants to model imperative computations, it seems like an imperative language would make for easier embedding. In an imperative language, each AST node can be given its own unique ID when it's constructed. So there's no trouble distinguishing between, e.g., calling some function bar() and then using the return value twice, versus calling bar() twice. No need to use a separate <- operator for assignments.
But to be fair, explicitly distinguishing side effects is kind of the whole point of Haskell!
(And if you wanted badly enough to do it in Haskell without requiring <-, you could use unsafePerformIO to assign AST node IDs.)
- eru 2y agoYou can do what you are describing in Haskell just fine. Eg you can implement 'overloaded' arithmetic operations for your custom data types. I think what's described in the article goes a lot further than this. (Though I'm not sure it's worth it.) Compare also https://okmij.org/ftp/tagless-final/ https://okmij.org/ftp/tagless-final/
- comex 2y agoI didn't mean to imply that you couldn't do the same in Haskell without the extra type-checking tricks in the article. I guess my main point is that the actual runtime semantics – like, if you take away the type checking, which functions are being called on what – are no different from what you'd see in Python. That differs from a lot of other Haskell techniques, where a direct port to Python would include tons of nested lambdas (which are relatively awkward in Python, and indeed in most imperative languages), and would also be fairly error-prone without the type checker to keep things straight. Even relatively simple uses of do notation fall into that category.
- eru 2y agoYou can directly port the Python technique you mentioned without any extra closures. (I mean not more than the normal amount of closures you'd get from straight-forward code that just calculates the answer as a number, instead of giving you the AST.)
- cryptonector 2y agoIn Python you get run-time typing and dispatch. In Haskell you get static typing [and monomorphisation]. GP means that static typing is safer and faster.
- eru 2y agoYou can do the same AST trick in a type safe manner.
- _jackdk_ 2y agoThe trick you describe is possible in Haskell also. https://hackage.haskell.org/package/simple-reflect https://hackage.haskell.org/package/simple-reflect does it by providing a `Num` instance for a particular type. I'm surprised that you say an imperative language would give you an _easier_ embedding than using Haskell. I would have said the reverse: Haskell makes it impossible to confuse an imperative command used during AST assembly with a representation of an imperative command. That's one of the payoffs of using monads.
- JadeNB 2y ago> I'm surprised that you say an imperative language would give you an _easier_ embedding than using Haskell. I don't think your parent says this. The three comparisons I found were: > The fun thing is that this sounds like "just" a more type-safe version of the way DSLs are usually implemented in imperative languages, particularly scripting languages. This sounds like saying that this trick is already common in imperative languages, not necessarily easier. > The DSL in question tries to block that by preventing AST nodes from being converted to boolean, but it's not perfect. Haskell has an advantage there. This explicitly gives the win to Haskell. > So there's no trouble distinguishing between, e.g., calling some function bar() and then using the return value twice, versus calling bar() twice. No need to use a separate <- operator for assignments. This one seems to give the win to imperative languages, but, as far as I can tell, just on the basis of syntax.
- pinkwinds 2y agoCool! Where could one go to read more about this (specifically Python)?