Help / JforC / Writing Your Own Modifiers

From J Wiki
Jump to navigation Jump to search

>> << Pri JfC LJ Phr Dic Voc !: Rel NuVoc wd Help J for C Programmers

                                                                      30. Writing Your Own Modifiers

If you find that you are coding recurring patterns of operations, you can write a modifier that represents the pattern.  You will find that reading your code is easier when the patterns are exhibited with names of your choosing.

You write a modifier like you write a verb, using conjunction define or adverb define, or 2 :n or 1 :n for one-liners.  When you assign the modifier to a name, that name becomes a conjunction or adverb, and it will be invoked as name v y (monad) or x u name v y (dyad) if it is a conjunction, or name y (monad)or x u name y (dyad) if it is an adverb.

When a modifier is invoked, the lines of the modifier are executed one by one, just as when a verb is invoked, and the result of the last sentence executed becomes the result of the modifier.  The u (and v, for conjunctions) operand(s) of the modifier are assigned to the local names u (and v) when the modifier starts execution (in addition, if u is a noun, it is assigned to the local name m and if v is a noun it is assigned to n)

User-written modifiers are of two types: those that refer to the variables x and y, and those that do not.

In early versions of J, operands were named x., y., u., v., m., and n. rather than x, y, u, v, m, and n .  The old form is obsolete.  If you want to use it you must execute 9!:49 (1) to enable recognition of the old forms.

Modifiers That Do Not Refer To x Or y

If the modifier does not refer to x or y, its text is interpreted when its operands (u and, for conjunctions, v) are supplied, and its result is an entity which may be any of the four principal parts of speech.  The result replaces the modifier and its operands in the sentence, and execution of the sentence continues.

For example, if the explicitly-defined conjunction c, which does not refer to x or y, is invoked as

   x u c v y

the sequence u c v is evaluated to produce a resulting verb, and then that verb is executed with x and y as operands.  The text of c is executed without reference to x and .

Usually you will want your modifier to produce a verb, but nothing keeps you from writing a conjunction whose result is, for example, another conjunction.  Here we will confine ourselves to verb results.

Let's write some of the utility modifiers referred to in earlier chapters.  Ifany was an adverb that executed u if y had a nonzero number of items:

   9!:3 (5)  NB. Do this once to select simplified display
   Ifany =: 1 : 'u ^: (*@#@])'
   < Ifany

Ifany does not need to look at y; it creates a verb that executes u only if y has items.  Here we have executed the adverb Ifany with the left operand <, and the result is a verb--the compound verb <^:(*@#@]) .  We can execute that verb on a noun operand:

   < Ifany 1 2 3
|1 2 3|

Remember that Ifany is an adverb, so it has precedence and the line is executed as if (< Ifany) 1 2 3 .  The verb (< Ifany), which has the value <^:(*@#@]), is applied to 1 2 3 and produces the boxed result.

   < Ifany ''

An empty y is left unboxed.

u Butifnull n was a conjunction that applied u if y had items, otherwise it produced a result of .  It could be written:

   Butifnull =: 2 : 'n"_ ` u @. (*@:#@:])'

Again (*@:#@:]) will check whether y has items, and this time the result will be used to select the appropriate verb to execute.

   < Butifnull 5

When Butifnull is executed with operands, it produces a verb.

   < Butifnull 5  'abc'
   < Butifnull 5  ''

The verb it produces can be applied to its own noun operands.

Example: Creating an Operating-System-Dependent Verb

The great thing about modifiers that do not refer to x or y is that they are fully interpreted before the x and y operands are supplied, so there is no interpretive overhead during the processing of the data.  Here is a more complex example taken from the J system.  The goal is to define a verb playsound that can be used to play a .wav file under Windows:

NB. y is the file data to be played
playsound =: '' adverb define
select. 9!:12 NIL
case. 2 do.
  'winmm.dll sndplaysound i *c i' & (15!:0) @ (;&1)
case. 6 do.
NB. 2=nodefault + 4=memory  +  16b20000 = file
  'winmm.dll PlaySound i *c i i' & (15!:0) @ (;&(0;4))

To begin with, let's make sense of this odd sequence  adverb define .  The adverb define defines an adverb, but what's the ?  Simple--it's the left argument to the adverb that was defined: the adverb is executed with u set to  .  The result of that execution of the adverb is what gets assigned to playsound .

      So, what happens when the adverb is executed?  The adverb calls the foreign 9!:12 to see what operating system is running, and executes a selected line that contains the definition of a compound verb.  Since that line is the last one executed, it becomes the result of the adverb; so the result of the adverb is the selected verb, and that is what is assigned to playsound .  On my system, this leaves playsound defined as a single compound verb:

'winmm.dll PlaySound i *c i i'&(15!:0)@(;&(0;4))

Lovely!  No check for operating system needs to be made when I invoke playsound; the check was made when playsound was defined.

Example: The LoopWithInitial Conjunction

The conjunction LoopWithInitial that we learned about earlier can be written as

   LoopWithInitial =: 2 : 'u&.>/\.&.(,&(<v))&.|.&.(<"_1)'

It's just one application of &. after another.  We can use it to illustrate a subtlety about modifiers that you should be aware of.  Consider an invocation of LoopWithInitial :

   vb =. +
   init =. 4 5
   vb LoopWithInitial init
vb&.>/\.&.(,&(<4 5))&.|.&.(<"_1)

The verb that is produced seems in order, but notice one point: the verb contains the value of init, but the name of vb .  This is a rule: the name of a verb argument is passed into a modifier, but the value of a noun argument is passed.  Note that if these lines appear inside a verb, the verb vb, which is assigned by private assignment, is not defined inside LoopWithInitial, because LoopWithInitial is running in a different explicit definition from the one in which vb was assigned.  As we see above, LoopWithInitial can pass vb into other modifiers, but if LoopWithInitial tried to execute vb it would fail.

Before we move on I want to point out one tiny example of the beauty of J.  For &.(,&(<4 5)) to work, there must be some obverse of ,&(<4 5) that undoes its effect.  What would that be?  We can see what the interpreter uses:

   ,&(<4 5) b. _1
}: :.(,&(<4 5))

It undoes the addition of a trailing item with }: which discards the last item.  Yes, that makes sense (the obverse has its own obverse which is the original verb).

Example: A Conjunction that Analyzes u and v

The conjunction u&.v expresses with great clarity the sequence of applying a transformation v, then applying the operation u, then inverting the transformation .  The dyad x u&.v y applies the same transformation to both x and y, but in many cases the transformation is meaningful only on one operand, and what we would like is a conjunction Undery such that x u Undery v y produces v^:_1 x u v y .  For example, to encipher the characters of y by replacing each one by the letter x positions earlier, we would use

   5 1 3 2 -~ Undery ('abcdefghijkl'&i."0) 'hijk'

to perform the function

   t =. 'abcdefghijkl'&i."0 'hijk'
   t =. 5 1 3 2 -~ t
   t { 'abcdefghijkl'

With that x stuck in the middle of the desired result v^:_1 x u v y  it appears that we will have to refer to x in our conjunction, but actually we can use an advanced feature of J to make the x disappear.  The sequence (u v) produces a verb that, when executed as the dyad x (u v) y, gives the result of x u v y (you will learn about this and more if you persevere with the part of the book devoted to tacit programming).  So, the verb we are looking for is v^:_1 @: (u v) and we can write

   Undery =: 2 : 'v^:_1 @: (u v)'
   5 1 3 2 -~ Undery ('abcdefghijkl'&i."0) 'hijk'

Before we pat ourselves on the back for this achievement, we should consider whether the verb produced by Undery has the proper rank.  We see that it does not: Undery applies v to the entire y, and u to the entire x and the result of u y, when really we should be performing the operation on cells of x and y, where the cell-size of x is given by the left rank of u and the cell-size of y is given by the right rank of .  For example, if we wanted to take the -x least-significant bits of y, we could use

   _3 {."0 1 Undery #: 30 

(remember that monad #: converts an integer y to its binary representation, producing a Boolean list--we are taking x bits of that and then converting back to integer)  The binary code for 30 is 11110, the 3 low-order bits are 110, and the result is 6.  But when we have list arguments, we get an incorrect result:

   _3 _4 _3 {."0 1 Undery #: 32 31 30 
0 15 12

The result for 30 is wrong: because #: was applied to the entire y, 110 was extended with framing fills to become 1100, and the result is 12 instead of the expected 6.  To get the right result we need to apply the verb to cells of the correct size:

   _3 _4 _3 ({. Undery #:"0) 32 31 30 
0 15 6

and naturally we would like to make Undery automatically produce a verb with the correct rank.

The way to find the rank of the verb u is to execute u b. 0 .  In our conjunction u and v are verbs, and we can use their ranks to produce an Undery that gives the correct rank:

   Undery =: 2 :'(v^:_1)@:(u v)"((1{u b.0),2{v b.0)'

We have selected the left rank of u and the right rank of v, and put them as the ranks of the verb produced by Undery .  This produces the desired result:

   _3 _4 _3 {."0 1 Undery #: 32 31 30 
0 15 6

and we can see the verb produced by Undery, with its ranks:

   {."0 1 Undery #:
#:^:_1@:({."0 1 #:)"0 0

This version of Undery produces correct results, but we should add one small improvement: the inverse of monad #: should be monad #. rather than monad #:^:_1, because the two forms are different.  One difference is obvious: the rank of #:^:_1 is infinite, while the rank of #. is 1; but that is immaterial in Undery .  The other difference is subtle but it could be significant: the two forms may have different performance in compounds.  The interpreter recognizes certain compounds for special handling; the list grows from release to release, but it's a pretty safe bet that #. will be selected for special treatment before #:^:_1 (and < before >^:_1, and so on).  So, we would like to replace the v^:_1 with the actual inverse of v .  We can get the inverse of v by looking at v b. _1 which produces a character-string representation of the inverse of .  We can then convert this string to a verb by making it the result of an adverb (we can't make it the result of a verb, because the result of a verb must be a noun).  So, we are led to

  Undery=:2 :'(a: 1 :(v b._1))@:(u v)"((1{u b.0),2{v b.0)'

where we defined the adverb 1 :(v b._1) and then immediately executed it with an ignored left operand a: to create the desired verb form.  Now we have

   {."0 1 Undery #:
#.@:({."0 1 #:)"0 0

which we can be content with.

An Exception: Modifiers that Do Not Refer to u or v

In very early versions of J, modifiers could not refer to their x and y operands.  In those days, a modifier used the names x and y to mean what we now mean by u and .  Modern versions of J continue to execute the old-fashioned modifiers correctly by applying the following rule: if a modifier does not contain any reference to u, v, m, or n, it is assumed to be an old-style modifier, and references to x and y are treated as if they were u and .  You may encounter old code that relies on this rule, but you should not add any new examples of your own.

Modifiers That Refer To x Or y

Most of the modifiers you write will refer to x and y .  The names x and y refer to the noun operands that are supplied when the modifier is invoked as [x] u adverb y or [x] u conjunction v y .

Here is an example, which is invoked as [x] u InLocales n y, where u is a verb and n is a list of locale names; it executes u y (or x u y if the invocation is dyadic) in each locale of :

InLocales =: 2 : 0

l1 =. 18!:5

for_l. n do.

  cocurrent l

  u y


cocurrent l1


l1 =. 18!:5

for_l. n do.

  cocurrent l

  x u y


cocurrent l1


This illustrates the important points.  The text of the definition is not interpreted until the x and y are available, in other words until the verb defined by u InLocales n is invoked.  Since that invocation may be either monadic or dyadic, two versions of the conjunction are given, one for each valence.  The result of the execution must be a noun, because the definition defines a verb and the result of a verb is always a noun.

That last point is important and I want to emphasize it.  It is true that InLocales is a conjunction, and yet its text defines a verb.  How is this possible?  Because InLocales is executed as a conjunction at the time it gets its u and n operands, but its text is not interpreted until the derived verb (which consists of u, n, and the text of InLocales) gets its x and y operands.  When InLocales is supplied with u and n, it is executed to produce a verb which consists of the text of InLocales along with u and the value of .  This derived verb is hidden inside the interpreter where it waits to be applied to a y (and possibly x).  When the derived verb is given its operands, it starts interpreting the text of InLocales, which was unusable until the time that y and x could be given values, and initializes u and n from the values that were saved when u InLocales n was executed.  Thus the text of InLocales describes a verb operating on y and .

Your modifiers should refer to x and y only if necessary.  Ifany from the previous section could have been written

   Ifany =: 1 : 'u^:(*#y) y'

which would produce exactly the same result as the other definition, but it would usually be slower, because the text could not be interpreted until x and y could be defined.  If the conjunction happens to be used in a verb of low rank, the result could be soporific.

Here's a puzzle that may be of interest to those readers whose eventual goal is Full Guru certification.  Why did InLocales save and restore the current locale?  Didn't we say that completion of any named entity restores the original locale?

Let's see what happens when we don't restore, using a simple testcase:

   t =: 1 : 0
cocurrent u
   (<'abc') t 0
   18!:5 ''

Sure enough, the current locale was changed!  But see what happens when we give a name to the verb created by the execution of :

   cocurrent <'base'
   tt =: (<'abc') t
   tt 0
   18!:5 ''

The current locale was restored.  What causes the difference?

The answer is that in the sentence (<'abc') t 0, the named adverb t is executed when it is given its operand <'abc' .  The result of that execution is the derived verb (<'abc') t which has no name.  When the derived verb is executed with the operand 0, the text is interpreted, causing a change to the current locale, and when the derived verb finishes, the current locale is not restored because the derived verb is anonymous.  If we give that derived verb a name (tt here), it restores the current locale on completion.

The observed behavior reinforces the point that the text of a modifier that refers to x or y is not interpreted when the modifier is executed; it is interpreted only when the derived verb is executed.

>> << Pri JfC LJ Phr Dic Voc !: Rel NuVoc wd Help J for C Programmers