2
\$\begingroup\$

I wrote a simplified pig latin translator in APL and I would like some feedback on it, as I am not sure my implementation is neat enough.

The simplified pig latin translation follows the following rules, where only 'aeiouAEIOU' are considered vowels:

  • one-letter words have 'way' appended to them; e.g. 'I' becomes 'Iway'
  • 2 or more letter words starting with a vowel have 'ay' appended to them; e.g. 'awesome' becomes 'awesomeay'
  • 2 or more letter words starting with a consonant have the consonants in front of the first vowel moved to the back, and then 'ay' is appended; e.g. 'cool' becomes 'oolcay'

The problem statement (problem 3 of the second easy problem set) specified that input might be a scalar (i.e. a single character) or a (possibly empty) character vector.

This is the code I wrote:

PigLatin ← {
  ⍝ Monadic function expecting character scalar or character vector and returning character vector.
  ⍝ Translates an English sentence into Pig lating.
  ⍝ e.g. 'I always run fast' becomes 'Iway alwaysay unray astfay'.

  vowels ← 'aeiouAEIOU'
  words ← ' ' (≠⊆⊢) ,⍵
  ⍝ Rotate all words until a vowel is at the front.
  rotated ← {⍵ ⌽⍨ ¯1+⊃⍸ ⍵∊vowels}¨ words
  ⍝ Append a 'w' to words of length 1.
  suffixed ← (,∘'w')¨@(1∘=≢¨) rotated
  ⍝ Append 'ay ' to all words, join and drop last ' '.
  ¯1↓∊ (,∘'ay ')¨ suffixed
}

Questions

  • The basic idea is that I split the input sentence into words, apply the rules to each word and then join them together; this seems sensible, right? This feels like a very standard algorithm but I don't know if APL is suitable for another type of approach.

  • Following the idea outlined above, my first version had this final line ∊ {⍺' '⍵}/ (,∘'ay')¨ suffixed instead of the current ¯1↓ ∊(,∘'ay ')¨ suffixed; but this meant my code didn't work for empty inputs '' because it tried running {⍺' '⍵}/ on an empty vector and raised a DOMAIN ERROR. My workaround for this was appending 'ay ' to every word, instead of just 'ay' and then dropping the final extra ' ' with ¯1↓;

    • Is this a good way of handling the edge case ''?
    • Would it be better if I had a dfn guard for the '' case?
    • Would you handle it in a different way?
  • Is ≠⊆⊢ an idiom in APL to split the right vector on the left arguments? It even shows in the tooltip for the Partition glyph.

  • Any further comments, suggestions, etc that don't necessarily address my questions are also welcome.

\$\endgroup\$
3
\$\begingroup\$

Overall

Your approach is fine, and your code (including ≠⊆⊢) is fairly idiomatic. Handling the edge case by always appending a space and dropping it at the end is standard procedure, so no, you don't need a branch here.

Split up your code in sections

You begin with setting up a couple of constants. Consider inserting a blank line to gently separate these from the main code.

Inline comments

Well-written APL code tends to have short lines, so there's generally enough space to include comments. This allows a simple hierarchy of comments:

  • Full-line comments for introductions to sections.
  • End-of-line comments for code explanations.

Consistency

You use intermediary variables for rotating and appending "w" but not for appending "ay ".

Array approach to conditional concatenation

(,∘'w')¨@(1∘=≢¨) does two loops:

  1. (1∘=≢¨) (which could be (1=≢¨) too) to determine which words need appending.
  2. (,∘'w')¨ (which could be ,∘'w'¨ or 'w',¨⍨ too) to do the appending.

A more holistic array approach is to append to every word, and instead modify what is appended. That is, collapse the appendix to shape 0 for words of length different from 1. Rephrased, this becomes keep the appendix as-is for words of length equal to 1, or 'w'/⍨1=≢. It becomes a "conditionally" append function in the form of ⊢,'w'/⍨1=≢, which you could then apply to each with (⊢,'w'/⍨1=≢)¨. However, you might want to…

Reduce ¨pepper¨

Some APLers call code with too many ¨s "too peppered" referring to the many tiny dots in foods that contain lots of black pepper. You may want to consider fusing the loops by defining the constituent transformation functions and applying them together in a single loop. Appropriate naming of the functions allows shortening the comments to become clarifications of the names, which can even make a comment obsolete.

Revised code

PigLatin←{
  ⍝ Monadic function expecting character scalar or character vector and returning character vector.
  ⍝ Translates an English sentence into Pig Latin.
  ⍝ e.g. 'I always run fast' becomes 'Iway alwaysay unray astfay'.

  vowels ← 'aeiouAEIOU'
  Words ← ' '(≠⊆⊢),

  Rotate ← {⍵ ⌽⍨ ¯1+⊃⍸ ⍵∊vowels}  ⍝ all words until a vowel is at the front
  Add_w ← ⊢,'w'/⍨1=≢              ⍝ if word has length 1
  Add_ay ← ,∘'ay '

  ¯1↓∊ Add_ay∘Add_w∘Rotate¨ Words ⍵
}

Other approaches

Writing APL is fun*, so APLers tend to write everything from scratch every time, instead of using the tools at hand. In this case, Perl-style regular expressions might be a help.

Using regex to process words

It is easy to apply a function to each word using '\w+' ⎕R {MyFn ⍵.Match}:

PigLatinWord←{
  vowels ← 'aeiouAEIOU'

  Rotate ← {⍵ ⌽⍨ ¯1+⊃⍸ ⍵∊vowels}
  W ← ⊢,'w'/⍨1=≢
  Ay ← ,∘'ay'

  Ay W Rotate ⍵
}
PigLatinRegex ← '\w+' ⎕R {PigLatinWord ⍵.Match}

The \w+ pattern matches runs of word characters.

If this was a common thing, we could define a utility operator that applies a text transformation on words:

_OnWords ← {'\w+' ⎕R (⍺⍺{⍺⍺ ⍵.Match}) ⍵}
PigLatinOnWords ← PigLatinWord _OnWords

An alternative coding which avoids passing the operand multiple times:

_OnWords ← {'\w+' ⎕R (⍺⍺⍎∘'Match') ⍵}
PigLatinOnWords ← PigLatinWord _OnWords

Doing the entire job with regexes

That said, ⎕R actually has a fancy feature that allows running multiple search patterns in parallel (for every starting character the patterns are tested in order) each with their own substitution pattern. This makes it easy to catch and process edge cases before the main transformations have a chance to kick in.

PigLatinRegexes ← '\w\b' '([^aeiou ]*)(\w+)' ⎕R '&way' '\2\1ay' ⍠1

Here, we have two patterns:

  1. \w\bword character, word boundary: a 1-character word.
  2. ([^aeiou ]*)(\w+) any consonants (group 1), word characters (group 2): any other word

And the corresponding substitution patterns:

  1. &way the match followed by "way"
  2. \2\1ay group 2, group 1 (which can be empty), "ay"

Finally, ⍠1 makes the derived function ignore case.

\$\endgroup\$
6
  • \$\begingroup\$ Adám, in your array approach to conditional concatenation, the 3 final snippets of code all have an extra , and a missing rho, I believe! \$\endgroup\$
    – RGS
    Apr 12 '20 at 22:30
  • \$\begingroup\$ @RGS Indeed. I've now made it / instead of too as that is more general; it works for vector appendices too. \$\endgroup\$
    – Adám
    Apr 12 '20 at 22:34
  • \$\begingroup\$ i.e. with / I could append 'www' or any other character vector, right? That was a really clever hint! \$\endgroup\$
    – RGS
    Apr 12 '20 at 22:44
  • \$\begingroup\$ @RGS That's correct. \$\endgroup\$
    – Adám
    Apr 12 '20 at 22:44
  • \$\begingroup\$ In the subsection Using regex to process words I think you missed a + in the regex when you say "It is easy to apply a function to each word using '\w' ⎕R {MyFn ⍵.Match}" \$\endgroup\$
    – RGS
    Apr 17 '20 at 22:31
3
\$\begingroup\$

Think about edge cases

Although the original problem statement doesn't mention it (nor the test cases provided), I can think of at least two kinds of edge cases:

  • Handling extraneous spaces (leading spaces, trailing spaces, or multiple spaces between words, e.g. __I___like__blanks___)
  • Handling capitalization (e.g. Creep -> eepCray or Eepcray?)

Notably, your solution does not preserve spaces (except for single space between words) while Adám's regex solution preserves all spaces. How would you preserve spaces without regex? There are multiple ways to chunk a string into words, preserving spaces:

  • Allow multiple leading blanks on each word: '__I' '___like' '__blanks' '___'. Alternatively, allow multiple trailing blanks: '__' 'I___' 'like__' 'blanks___'.
  • Allow single leading (resp. trailing) blank on each word: '_' '_I' '_' '_' '_like' ....
  • Allow blanks to form their own chunks: '__' 'I' '___' 'like' .... See dfns.words.

Each choice can make some parts easy but some other parts harder. Be sure to explore various possibilities and pick the one you like the most.

Nitpicking: Avoid unnecessary

In your code, ¯1+⊃⍸ is essentially counting leading zeros on a boolean array. But the monadic is pretty heavy, and requires ⎕IO adjustment. APLcart gives the entry (⊥⍨0=⌽)Bv for the query "leading zero". By unpacking the train, you can use boolean negation ~ instead of 0=:

⍝ Instead of this
¯1+⊃⍸ ⍵∊vowels
⍝ Do this
⊥⍨⌽ ~⍵∊vowels

Note that ⊥⍨ on a boolean vector is a (very clever) idiom for "count trailing ones".

\$\endgroup\$
1
  • \$\begingroup\$ Nice use of the APL Cart! I like the ⊥⍨ :D \$\endgroup\$
    – RGS
    Apr 13 '20 at 18:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.