Re: [stack] Re: How the Interpreter Works

John Nowak <[email protected]>
Newsgroups gmane.comp.lang.concatenative
Message-ID <[email protected]>
On Apr 15, 2008, at 2:04 PM, William Tanksley, Jr wrote:

>> So, back to concatenative programming.  Most if not all proofs
>> done in Haskell rely on the type system.  The Haskell type
>> system enforces referential transparency.  Cat is the only stack
>> language I know of that has a type *system* that might be up
>> to the task.  Yet I hear people on this list claiming that the
>> property of concatenativity alone will get you to a place where
>> formal methods become practical.  What about it?
>
> Well, a type system is itself a formal method; it's just a subset of
> more extensive automated proofs. Fifth is another concatenative
> language that, in addition to an inferenced type system, has an
> effects system. I don't know whether its effects system is influenced
> or benefitted by its concatenativity.

The concatenative nature of Fifth *hugely* simplifies things. Let's  
assume for the moment that all of the data structures in Fifth are  
persistent/immutable. The effect system therefore only needs to track  
IO (reading from a file, generating random numbers, etc). We can  
actually add this effect tracking to Fifth without any modifications  
to the type system.

(This probably gets a bit confusing as I wrote this very quickly; skip  
to the end for the summary if you want.)

As has been mentioned here previously, we can conceptualize functions  
that perform IO as reading/writing some "world" value that gets  
threaded through the entire program on the top of the stack. This  
threading is easy to accomplish because of the monoidal nature of  
concatenative language. To be more clear, the 'dup' function normally  
has the following type:

    dup :: R a -> R a a

However, if we were to actually include the "world" in the type, it  
would look like this:

    dup :: R a b -> R a a b

The reason we just use a variable for the world is that 'dup' doesn't  
access it; it just passes it along. However, some functions do modify  
the world. For example, the type of the 'display' function that prints  
a string to the screen would be as such:

    display :: R String a -> R Impure

This type says that display takes some stack 'R' with a 'String' as  
the second element and a world value of any type on top. The display  
function then yields the stack 'R' with an "impure" world on the top.  
In other words, display takes a string and any world and yields a  
string with a modified world.

Now if we look back at the type of 'dup', we can see that composing  
'display' and 'dup' would result in the impure world type being  
propagated forward as 'dup' is simply "a -> a" as far as the world is  
concerned. Essentially, if 'display' is impure, then so is 'display  
dup'.

Finally, we get to the point: If we mandate a world that is "pure",  
then we can enforce that a given function has no side effects. For  
example, here's the full type of 'map' (a simpler type is shown to the  
user):

    map :: R (List a) [R a Pure -> R b Pure] c -> R (List b) c

So what does this say? Well, it's clear that map needs a list. It also  
needs a function. However, the presence of 'Pure' in this function  
says that if given a pure world, the purity of that world must be  
preserved. Accordingly, any function provided to 'map' is guaranteed  
not to have side effects. Note however that 'map' itself can be  
composed in between functions that do have side effects.

Here's one more example of why this system is very nice. The 'o'  
function (called 'compose' in Cat and '.' and Haskell) composes two  
functions on the top of the stack to yield a new function. Including  
the world (again, this is presented more concisely to the user), it  
has the following type:

    o :: R [S a -> T a] [T a -> U a] b -> R [S a -> U a] b

As you can (maybe) see from this type, the manner in which the world  
is handled by the resulting composed function depends on how the two  
functions provided to 'o' handle the world. However, even if these two  
functions have effects and modify the world, this will not make the  
call to 'o' itself impure because its world variable is 'b', not 'a'  
as it is for the functions to be composed.

- - -

I've likely lost you by now, as my explanation isn't very clear, but  
the point is that you get a very useful system that can be used to  
enforce purity -- without the composability headache of monads -- for  
free! If you add another variable to the top of the stack and disallow  
recursive definitions and non-terminating self-application (the  
"occurs check" that prevents infinite types does the latter for you),  
you can track and ensure termination as well!

You might be wondering if you could add yet another variable to track  
mutations to data structures. The answer is that you *can* but the  
results will be too conservative; it becomes impossible to write a  
pure function that is implemented using impure means. To handle this  
properly, we can again do so through adding variables, but these need  
to be attached to the individual parameterized types for the mutable  
values rather than passed on top of the stack. A small addition to the  
type system is then required to handle the enforcement of purity  
properly. However, this is just a 10 line addition to the ~80 line  
type system core type system. (The "non-core" features include  
implicit parameters, aka "globals done right", as well as tracking of  
exceptions.)

- John
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.