Readable Tacit Definitions

<< >> Contents Help

# 39. Readable Tacit Definitions

Heron's formula for the area of a triangle, given the lengths of the sides a, b, and c, is sqrt(s*(s-a)*(s-b)*s-c)) where s is (a+b+c)%2 . This can be written

triarea =: [: %: [: */ -:@:(+/) - 0: , ]

triarea 3 4 5

6

Regardless of your diligence in commenting your code and the level of J expertise in you and the sorry wretches who will have to read it, long tacit definitions like this can become trackless wastelands, Saharas of verb following verb unendingly with nothing to suggest an interpretation of the intermediate results. I know of two ways to mark a trail through such a definition. The first is to develop a regimen for using spaces and parentheses to help the reader group the parts. I would write

triarea =: [: %: [: */ (-:@:(+/)) - 0:,]

The second way is to split the line into multiple lines, where each line can have a comment or a verb-name indicating what it produces. This approach, carried almost to an extreme, would yield

semiperimeter =: -:@:(+/)

factors =: semiperimeter - 0:,]

product =: [: */ factors

triarea =: [: %: product

Combining the two approaches, you can find a comfortable level of complexity.

Flatten a Verb: Adverb f.

Splitting the definition into many lines has the unfortunate side-effect that all the names referred to by triarea must be defined when triarea is executed:

triarea

[: %: product

triarea refers to product which refers to factors which refers to semiperimeter . If you define many tacit verbs this way, the result is pollution of the namespace. To leave a smaller footprint, use private assignment for all the names except the name that will be public, and use the adverb f. which replaces names in its operand with their definitions:

semiperimeter =. -:@:(+/)

factors =. semiperimeter - 0:,]

product =. [: */ factors

triarea =: ([: %: product) f.

triarea

[: %: [: */ -:@:(+/) - 0: , ]

If these verbs are run from a script, the temporary verbs will disappear (since they were assigned by private assignment), leaving only the main verb triarea .

f. is also used in initialization of objects, as you can learn in the Lab for Object-Oriented Programming.

Note that f. has no effect on explicit definitions.

Using f. to improve performance

Flattening a verb has two beneficial effects on performance. The first is easy to see by comparing two equivalent sentences:

a =. i. 100000

abs0 =: 3 : '| y.' "0

6!:2 'abs0 a'

3.17136

abs1 =: 3 : '| y.'

6!:2 'abs1"0 a'

3.54908

To be sure, in each case we have committed the crime of applying an explicitly-defined verb at a low rank (|"0 a executes in time 0.006), but that is not the point. Why is abs1"0 slower than abs0? Each one reinterprets its verb for each atom of a .

The answer is that when abs1"0 is executed, the definition of abs1 must be looked up for every atom of a (for all the interpreter knows, abs1 might be redefined during its execution). The time spent doing this lookup accounts for the difference in time between abs0 and abs1"0 . If we eliminate the lookup of the name abs1, that time is saved:

6!:2 'abs1 f."0 a'

3.00941

The important lesson to learn from this is that you should define your verbs with the proper rank. That will eliminate superfluous name lookups. In exceptional cases you may use f. to avoid name lookups during execution of a complex verb.

The second case where f. can improve performance is useful only for those users who feel compelled to redefine the J primitives with mnemonic names. This is a practice that I strongly deprecate, and if you don't heed my advice, the interpreter stands ready to punish you. See the disaster that can result when the primitives are replaced by mnemonic names:

tally =: #

a =. 100000 $ i. 6

b =. i. 100000 10

6!:2 'a #/. b'

0.0157688

6!:2 'a tally/. b'

0.154675

What happened is that #/. is handled by special code in the interpreter, but tally/. is not. The fact that tally is defined to be # is immaterial: the interpreter doesn't know that at the time it creates the anonymous verb for tally/. . The penalty is an almost-tenfold increase in execution time.

6!:2 'a tally f./. b'

0.0167351

By flattening tally, we cause it to be replaced by its definition #, and then the special case #/. is recognized.

b =. 0 = i. 100000

6!:2 '(# i.@#) b'

0.0011616

6!:2 '(tally index@tally) b'

0.00385943

Another example: (# i.@#) is handled by special code, but the names prevent the interpreter from recognizing the situation.

6!:2 '(tally index@tally) f. b'

0.0011861

If we flatten every verb, we get good performance, but what an effort! It's much better to use the J primitives directly, so the interpreter can do its job effectively.

<< >> Contents Help