jsoftware / jsource

J engine source mirror
Other
657 stars 90 forks source link

[feature request] first class verbs #116

Open hoosierEE opened 2 years ago

hoosierEE commented 2 years ago

The idea of "first class verbs" or more commonly "first class functions" is a general principle that gives a language the following features:

Today some of the features of first class verbs already exist in J, but have limitations and require verbose syntax.

I'll put some examples below to show how this could look in future J code.

accept verbs as parameters

This is awkward in J, as it requires first turning the verb into a gerund with ` (tie), and then using `: (evoke gerund) to turn it back into a verb:

   ({{2<y}}`'') {{ ((x`:6) y) # y }} i.9
3 4 5 6 7 8

First class verbs simplifies the above example to this:

   NB. proposed change
   {{2<y}} {{ (x y) # y }} i.9
3 4 5 6 7 8

NOTE: The phrase {{ x y }} is currently a syntax error, so adding this feature would not break any existing code.

return verbs as values

This feature is often called a "closure" in other programming languages, because the inner scope "closes over" (inheriits) the variables from the outer scope.

Similar to the previous example, now returning a verb from another verb:

   f1 =: {{
s =. y
({{ s + y }}`'') }}
   ((f1 3)`:6) 10
13

However, I think gerunds may be buggy (perhaps only inside{{}}), because executing it with different arguments gives an unchanging result:

   NB. buggy behavior
   ((f1 3)`:6) 1
4
   ((f1 99)`:6) 1
4

The above should work like this instead:

   NB. correct behavior
   ((f1 3)`:6) 1
4
   ((f1 99)`:6) 1
100

Proposed enhancement: returning a verb and using that verb can work like this:

   NB. proposed change
   f1 =: {{
s =. y
{{ s + y }} }}
   (f1 3) 10 
13
   (f1 4) 10 
14

Currently, the explicit definition above is not a syntax error, but running (f1 3) is an error:

|noun result was required: f1
|       {{s + y }}

I don't think adding this feature (return verbs from verbs) would break any code, but I'm not sure.

rationale

HenryHRich commented 2 years ago

Allowing a verb to return a verb would affect the parser and the JE in general. If OTOH the result of a verb can be a noun gerund that is then executed as a verb, additionally there is the overhead of checking each result to see whether it can be executed.

The 'buggy behavior' is correct: s in the nested {{ }} is not inherited from the enclosing verb and is presumably a global left around in the author's session.

An idea to consider is a locale syntax that specifies a name in the caller's stack frame: s___1 to mean 's in the caller'. This would not help the nested-s issue, since the definition is not executed while the caller is in scope, but there might be some other uses for it.

hoosierEE commented 2 years ago

Thanks Henry, that is a good point about performance. I like the idea of "only pay for what you use", so while I would be happy to pay a performance penalty for code which uses this feature, code which does not use the feature shouldn't pay its cost.

I did not realize that direct definition verbs can only access variables either in the global scope or their own scope. I see now that it's not a bug, so perhaps that will become a separate feature request.

I realized after writing my feature request that I should give more examples. Here's an example where a closure which can refer to variables from its parent scope:

   NB. hypothetical version of J with proposed changes
   Seq =: {{
count =. y  NB. local to Seq
{{ count =. count + 1 }} }}  NB. names from the enclosing scope are like static variables in C functions
   s1 =: Seq 4
   s1 ''
5
   s1 ''
6
   s2 =: Seq _5  NB. make a new "count"
   s2 ''
_4
   s1 ''
7

J already has facilities (coclass etc.) that allow creating a functionally-identical program:

   NB. existing J902
   coclass'Seq'
   create =: {{ count =: y }}
   increment =: {{ count =: count + 1 }}
   destroy =: codestroy
   cocurrent'base'
   s1 =: 4 conew 'Seq'
   increment__s1 ''
5
   increment__s1 ''
6
   s2 =: _5 conew 'Seq'
   increment__s2 ''
_4
   increment__s1 ''
7

This may not be a great example, since there's not a huge difference syntactically - the closure is only a little shorter, and whether lexical scope is clearer is subjective.

When the (closure version) s1 goes out of scope, is erased, or overwritten, the interpreter could possibly automatically free both s1 and its count, whereas for objects created with conew, the programmer must "manually" use codestroy__s1''.

First class verbs are not essential for my work with J, but because they can mimic locales, classes, adverbs, and conjunctions with minimal syntax, I consider them a powerful tool of thought.