iPoe: interactive poetry editor
Check a poem against one of the built-in poetic forms (Using an iPoe Language).
Define a new poetic form (Defining a new iPoe Language).
Add basic English-language processing to a Racket program (iPoe API).
Each poetic form is implemented as a Racket #lang. The syntax of these languages is plain text; their semantics is to check the text for spelling, grammar, and other errors.
NOTE: this package is unstable. The purpose of this documentation is just to describe how ipoe fits together as of January 2017.
1 Using an iPoe Language
1.1 A Guided Tour
Let’s write a haiku. According to Wikipedia:
A typical haiku is a three-line observation about a fleeting moment involving nature. .... with 17 syllables arranged in a 5–7–5 pattern.
#lang ipoe/haiku |
|
pit pat pit pat, rain? |
or clicking on a keyboard? |
it's 3 am I don't care no more |
Save the file as ./first.haiku and run raco make first.haiku to compile the poem. Compiling will check the text of the poem against the ipoe/haiku poetic form.
Missing run-time parameter for database username. Please enter a username to connect to the ipoe database. |
Enter your database username (or #f to skip): |
ipoe> |
Just type #f and skip for now. (If you’re feeling brave, try running raco ipoe init or browsing Pragmatics.) The next thing you will see is:
Starting ipoe without a database connection (in online-only mode) |
Word 1 on line 1 of stanza 0 ('clicking') is undefined. Consider adding it to the dictionary. |
Word 4 on line 2 of stanza 0 ('dont') is undefined. Consider adding it to the dictionary. |
ipoe: Expected 7 syllables on Line 1 of Stanza 0, got 5 syllables |
compilation context...: |
/Users/ben/code/racket/my-pkgs/ipoe/ipoe/first.rkt |
Okay! This means ipoe checked our poem and found a problem: the second line has 5 syllables but was expected to have 7 syllables. The trouble is that ipoe couldn’t figure out how many syllables are in the word “clicking” (by asking the internet, see the ipoe/scrape module).
We can’t add “clicking” to the dictionary because we haven’t set up a database. But we can bypass the issue by claiming our poetic license. Change the poem to read:
#lang ipoe/haiku |
#:poetic-license 10 |
|
pit pat pit pat, rain? |
or clicking on a keyboard? |
it's 3 am I don't care no more |
Now compile the poem; you should see:
Missing run-time parameter for database username. Please enter a username to connect to the ipoe database. |
Enter your database username (or #f to skip): |
ipoe> #f |
Starting ipoe without a database connection (in online-only mode) |
Word 1 on line 1 of stanza 0 ('clicking') is undefined. Consider adding it to the dictionary. |
Word 4 on line 2 of stanza 0 ('dont') is undefined. Consider adding it to the dictionary. |
Finished checking poem. |
- Misspelled word 'clicking'; position 1 on line 1 of stanza 0. |
- Misspelled word 'dont'; position 4 on line 2 of stanza 0. Maybe you meant 'on'? |
- Expected 7 syllables on Line 1 of Stanza 0, got 5 syllables |
- Expected 5 syllables on Line 2 of Stanza 0, got 7 syllables |
Remaining poetic license: 6 |
The #lang line declares a poetic form. Poetic forms can specify the number of lines, number of stanzas, rhyme scheme, and number of syllables in the following text. See Defining a new iPoe Language for more details.
ipoe checks the internet to get the spelling, number of syllables, and rhymes for a given word. If you have an ipoe database, it stores this information. Otherwise, the data for words is cached locally (in ./compiled/ipoe.cache).
Poems can start with a few keyword/value pairs to alter the poem-checker.
1.2 iPoe Concepts
An iPoe poem is a text file with #lang <spec> as its first line, where <spec> refers to an iPoe poetic form.
A poetic form is a grammar; it specifies how a class of poems should look and sound.
A rhyme scheme is an important part of a poetic form; it declares the number of stanzas, number of lines in each stanza, number of syllables in each line, and constrains certain lines to rhyme with one another.
A iPoe stanza is a group of lines. Different stanzas are separated by at least 2 consecutive newline characters.
A iPoe line is a sequence of words. Different lines are separated by a single newline character.
A iPoe word is basically a sequence of non-whitespace characters. Different words are separated by at least one whitespace character. (This definition is intentionally vague.)
syllables are “units of pronunciation” in a word (thanks Google).
Two words rhyme if they end with the same sound.
Two words almost rhyme if they kind-of-but-not-really end with the same sound.
How much license? I don’t know. How much does it cost to break a rule? I don’t know, but I tried to set good defaults. Have fun with this until we have something better.
An iPoe database stores known words, their properties, and the relation between them. Once you have an iPoe database, it will update itself. But it tends to ask for too much help, you can disable the help by putting #:interactive? #f at the top of your poems or configuration file.
1.2.1 Built-in Poetic Forms
#lang ipoe/cinquain | package: ipoe |
#lang ipoe/clerihew | package: ipoe |
#lang ipoe/couplet | package: ipoe |
#lang ipoe/english-sonnet | package: ipoe |
#lang ipoe/free-verse | package: ipoe |
#lang ipoe/haiku | package: ipoe |
#lang ipoe/italian-sonnet | package: ipoe |
#lang ipoe/limerick | package: ipoe |
#lang ipoe/quaternion | package: ipoe |
#lang ipoe/rondelet | package: ipoe |
#lang ipoe/sestina | package: ipoe |
#lang ipoe/sextilla | package: ipoe |
#lang ipoe/tanaga | package: ipoe |
#lang ipoe/tercet | package: ipoe |
#lang ipoe/villanelle | package: ipoe |
1.3 Pragmatics
1.3.1 Connecting to an ipoe Database
If you install PostgreSQL and start a server, running raco ipoe init should start an iPoe database.
1.3.2 Configuration
Global configuration file: (writable-config-file ".ipoe" #:program "ipoe")
Local configuration file: "./.ipoe"
There are many configuration options. Put these in a configuration file or at the top of a poem. Global configurations have the least precedence, in-file configurations have the most.
#:user string? username for the iPoe database
#:dbname string? database name for the iPoe database
#:interactive? boolean? when #true, ask permission before peforming most actions. Otherwise, never ask for user input.
#:online? boolean? when #true, use the internet to look up information about words. Otherwise, never use the internet.
#:spellcheck? boolean? when #true, warn the user about spelling errors.
#:grammarcheck? boolean? when #true, warn the user about grammar errors (currently does nothing).
#:suggest-rhyme? boolean? when #true, offer suggestions to replace a word that doesn’t rhyme.
#:suggest-spelling? boolean? when #true, offer suggestions to replace a mis-spelled word.
#:poetic-license exact-nonnegative-integer? set value of initial poetic license
#:almost-rhyme-penalty (or/c exact-nonnegative-integer? #f) set penalty for using an almost-rhyme. By default, 1.
#:bad-extra-penalty (or/c exact-nonnegative-integer? #f) set penalty for failing a #:constraint. By default, 10.
#:bad-lines-penalty (or/c exact-nonnegative-integer? #f) set penalty for using the wrong number of lines. By default, #false; this immediately rejects poems with the wrong number of lines.
#:bad-rhyme-penalty (or/c exact-nonnegative-integer? #f) set the penalty for not rhyming. By default, 3.
#:bad-stanza-penalty (or/c exact-nonnegative-integer? #f) set the penalty for using the wrong number of stanzas. By default, #false.
#:bad-syllable-penalty (or/c exact-nonnegative-integer? #f) set the penalty for using the wrong number of syllables. By default, 1.
#:repeat-rhyme-penalty (or/c exact-nonnegative-integer? #f) set the penalty for using rhyming a word with itself. By default, 3.
#:spelling-error-penalty (or/c exact-nonnegative-integer? #f) set the penalty for misspelling a word. By default, 0.
2 Defining a new iPoe Language
The ipoe language is a specification language for poetic forms.
| ‹stmt› | ::= | #:name identifier |
|
| | | #:description string? |
|
| | | #:syllables exact-positive-integer? |
|
| | | #:rhyme-scheme ‹rhyme-scheme› |
|
| | | #:constraint ‹constraint-expr› |
| ‹rhyme-scheme› | ::= | ( ‹stanza-scheme›* ) |
| ‹stanza-scheme› | ::= | ( ‹line-scheme›* ) |
| ‹line-scheme› | ::= | ( ‹rhyme› . ‹syllable› ) |
|
| | | ‹syllable› |
|
| | | ‹rhyme› |
| ‹rhyme› | ::= | |
|
| | | ‹wildcard› |
| ‹syllable› | ::= | |
|
| | | ‹wildcard› |
| ‹wildcard› | ::= | * |
| ‹constraint-expr› | ::= |
The #:name statement must appear once. This statement declares the name of the poetic form (currently only used for debugging).
The #:description statement may appear at most once; it gives a brief description of the poetic form (for debugging).
The #:syllables statement may appear at most once. When #:syllables N is given, any wildcard syllables in the ‹rhyme-scheme› (implicit or explicit) are replaced with N.
- The #:rhyme-scheme statement may appear at most once. A ‹rhyme-scheme› specifies:
the number of stanzas in the poetic form,
the number of lines in each stanza,
the number of syllables in each line, and
which lines must rhyme with one another.
An empty rhyme scheme allows any number of stanzas. A ‹line-scheme› that omits the ‹rhyme› or ‹syllable› component has an implicit wildcard in that component. A ‹rhyme› can be any symbol; lines that must rhyme must have equal symbols (in the sense of eq?). The #:constraint statement may appear any number of times. Each statement may contain arbitrary expressions using (most!) identifiers from the racket/base and ipoe/poetic-form namespaces.
NOTE: The syntax of ‹constraint-expr› nonterminals is bad IMO. Ideally it should be a simple, Racket-like domain-specific-language. Allowing anything in racket/base is probably too much power, but ipoe/poetic-form by itself isn’t enough to express the constraints in certain poems (ipoe/sestina).
2.1 Installing a Language
2.1.1 The Fast Way
Install the gnal-lang package.
Create a directory my-form/ and a sub-directory my-form/lang/.
Save your program as my-form/lang/reader.rkt.
Now you can write programs using the new poetic form:
#lang gnal "my-form" |
|
Racket is fun because #:fun is a keyword! |
Compiling the program will check the text against the my-form specification.
2.1.2 The Franchised Way
Create the my-form/lang/ directory and save the ipoe program as my-form/lang/reader.rkt.
Run raco pkg install ./my-form.
Write programs that start with #lang my-form.
2.1.3 The Directory-Free Way
TODO should be possible to defined a reader submodule, but this doesn’t work right now!
2.2 Example Languages
Browse the source code for more examples.
#lang ipoe #:name haiku #:rhyme-scheme {[5 7 5]} #:description "1 stanza, 3 lines No rhyme constraints 5 syllables in first line 7 in second 5 in last"
#lang ipoe #:name villanelle #:rhyme-scheme {[R1 B R2] [A B R1] [A B R2] [A B R1] [A B R2] [A B R1 R2]} #:syllables 10 #:constraint (line=? (line 0 (stanza 0)) (line 2 (stanza 1)) (line 2 (stanza 3)) (line 2 (stanza 5))) #:constraint (line=? (line 2 (stanza 0)) (line 2 (stanza 2)) (line 2 (stanza 4)) (line 3 (stanza 5)))
3 iPoe API
The following modules have been useful internally to ipoe and are stable enough to be used in (or ported to) other projects. Hopefully, these will eventually re-appear in a more general context (scribble, sxml, pkgs.racket-lang.org, etc.).
3.1 English-Language Tools
(require ipoe/english) | package: ipoe |
> (infer-rhyme=? "cat" "hat") #t
> (infer-rhyme=? "sauce" "boss") #f
> (infer-rhyme=? "books" "carrots") #t
procedure
(infer-syllables str) → exact-nonnegative-integer?
str : string?
Technically, the number of syllables in some words depends on their pronunciation (does “flower” have 1 or 2 syllables?), but this function just returns a natural number without thinking very hard.
> (infer-syllables "month") 1
> (infer-syllables "hour") 2
> (infer-syllables "munificent") 4
procedure
i : exact-integer?
> (integer->word* 2) '("two")
> (integer->word* -21) '("negative" "twenty" "one")
procedure
(integer->word/space i) → string?
i : exact-integer?
(string-join (integer->word* i) " ")
procedure
(suggest-spelling str [ #:max-distance d #:limit n]) → (listof string?) str : string? d : exact-nonnegative-integer? = 2 n : exact-nonnegative-integer? = 10
> (suggest-spelling "tpyo") '("to" "two" "try" "too" "toy" "pro" "spy")
> (suggest-spelling "balon" #:limit 3) '("along" "alone" "below")
3.2 Poetic Form API
(require ipoe/poetic-form) | package: ipoe |
procedure
i : integer? p : poem? = (*current-poem*)
procedure
i : exact-nonnegative-integer? st : stanza?
procedure
i : exact-nonnegative-integer? ln : line?
procedure
(line->word* ln) → (sequence/c word?)
ln : line?
procedure
(poem->stanza* p) → (sequence/c stanza?)
p : poem?
procedure
(poem->word* p) → (sequence/c word?)
p : poem?
procedure
(stanza->line* st) → (sequence/c line?)
st : stanza?
procedure
p : poem?
procedure
st : stanza?
procedure
(last-stanza [poem]) → stanza?
poem : poem? = *current-poem*
procedure
(contains-word? ln w) → boolean?
ln : line? w : word?
parameter
(*current-poem*) → (or/c poem? #f)
(*current-poem* p) → void? p : (or/c poem? #f)
= #f
3.3 Web Scraping
(require ipoe/scrape) | package: ipoe |
procedure
(make-word-scraper sym #:word->url word->url #:sxml->definition sxml->defn #:sxml->num-syllables sxml->syll #:sxml->word sxml->word) → (-> string? (or/c word-result? #f)) sym : symbol? word->url : (-> string? string?) sxml->defn : (-> sxml (or/c string? #f)) sxml->syll : (-> sxml (or/c exact-nonnegative-integer? #f)) sxml->word : (-> sxml (or/c string? #f))
sym briefly describes the resource being scraped.
word->url converts a word to a URL for a web page that may have information about the word.
sxml->defn parses an sxml page for the definition of a word.
sxml->syll parses an sxml page for the number of syllables in a word.
sxml->word parses an sxml page for the word the page describes.
value
value
value
value
procedure
str : string?
procedure
(sxml-200? sx) → boolean?
sx : sxml:top?
I do not know the purpose of the second argument to the procedure returned by id?.
> ((sxpath `(// div ,(id? "foo") *text*)) '(div (div (@ id "foo") "foo-text") (div (@ id "bar") "bar-text"))) '()
procedure
(class? class-str [<?]) → (-> (listof sxml) any/c (listof sxml))
class-str : string? <? : (-> string? string? boolean?) = string=?
> ((sxpath `(// div ,(class? "a" string-prefix?) *text*)) '(div (div (@ class "abc") "success") (div (@ class "def") "failure"))) '()
value
scrape-logger : logger?
4 External Links
RacketCon 2015 slides (pdf), writeup (pdf), and video (YouTube).