The Big Book of Small EK9 Programs
Adapted from Al Sweigart's The Big Book of Small Python Projects: 81 Easy Practice Programs (No Starch Press, 2021; copyright © 2021 Al Sweigart), which he makes freely available under a Creative Commons Attribution-NonCommercial-ShareAlike licence at inventwithpython.com/bigbookpython. Changes were made: each program was ported from Python to EK9 and remodelled in EK9's idioms.
Project 11 — Clickbait Headline Generator
Clickbait Headline Generator writes fake tabloid-style headlines by filling in random blanks — a state, a noun, a pronoun, a number — drawn from word lists and dropped into one of eight fill-in-the-blank templates, as many times as the player asks for.
Adapted from project 11 of Al Sweigart's The Big Book of Small Python Projects (No Starch Press, 2021), under CC BY-NC-SA 4.0.
A sample run
Run with BIGBOOK_SEED=42 ek9 clickbait.ek9 < input.txt, answering the only
prompt with a count of 6 headlines:
6
By Al Sweigart al@inventwithpython.com
Our website needs to trick people into looking at ads!
Enter the number of clickbait headlines to generate:
This Georgia Serial Killer Didn't Think Robots Would Take His Job. He Was Wrong.
Big Companies Hate Her! See How This Ohio Parent Invented a Cheaper Cat
You Won't Believe What This Texas Dog Found in Her Workplace
What Clowns Don't Want You To Know About Paleo Diets
This Florida Robot Didn't Think Robots Would Take Her Job. She Was Wrong.
Big Companies Hate Them! See How This Georgia Serial Killer Invented a Cheaper Avocado
Post these to our Facesbook soon or you're fired!
The program
#!ek9 defines module bigbook.clickbait defines constant FewestGiftIdeas <- 7 MostGiftIdeas <- 15 FewestReasons <- 3 MostReasons <- 19 defines type HeadlineType Millennials WithoutThis BigCompanies YouWontBelieve DontWant GiftIdeas ReasonsWhy JobAutomated List of String List of HeadlineType List of Pronouns defines record //The original keeps three parallel pronoun lists and picks the same index from two of them. //One record per person keeps "Her", "She" and "Was" together, so they cannot disagree. Pronouns objective as String: String() possessive as String: String() personal as String: String() pastOfBe as String: String() Pronouns() as pure -> objective as String possessive as String personal as String pastOfBe as String this.objective :=? objective this.possessive :=? possessive this.personal :=? personal this.pastOfBe :=? pastOfBe default operator ? //One headline the game has drawn: its kind and the blanks that kind fills. A kind uses //only some of the blanks; the others stay unset. Headline kind as HeadlineType: HeadlineType() person as Pronouns: Pronouns() state as String: String() noun as String: String() otherNoun as String: String() place as String: String() timing as String: String() number as Integer: Integer() otherNumber as Integer: Integer() default operator ? defines class //THE input idiom of this book, copied unchanged from lib/LineInput.ek9 (the reasons are //there): read with next(), and every input loop also stops when ended() is true. LineInput stdin as Stdin: Stdin() inputEnded as Boolean: false next() <- rtn as String: stdin.next() if not rtn? inputEnded: true ended() as pure <- rtn as Boolean: Boolean(inputEnded) override operator ? as pure <- rtn as Boolean: true //THE INPUT AND OUTPUT: everything the player sees or types goes through here, worded by Say. Console stdout as Stdout: Stdout() input as LineInput: LineInput() say as Say: Say("en") Console() -> words as Say say: words showIntroduction() stdout.println(say.introduction()) //Asks until the reply is a number; unset only if the input ends first. nextCount() <- rtn as Integer: Integer() while not rtn? and not input.ended() stdout.println(say.askCount()) rtn: countFrom(input.next()) if not rtn? and not input.ended() stdout.println(say.notANumber()) showHeadline() -> headline as Headline stdout.println(headlineText(headline)) showSignOff() -> website as String timing as String stdout.println("") stdout.println(say.postThese(website, timing.lowerCase())) //Words a drawn headline with the template for its kind. private headlineText() -> headline as Headline <- rtn as String: String() switch headline.kind case HeadlineType.Millennials rtn: say.areMillennialsKilling(headline.noun) case HeadlineType.WithoutThis rtn: say.withoutThis(headline.noun, headline.otherNoun, headline.timing) case HeadlineType.BigCompanies rtn: say.bigCompaniesHate( pronoun: headline.person.objective, state: headline.state, noun1: headline.noun, noun2: headline.otherNoun ) case HeadlineType.YouWontBelieve rtn: say.youWontBelieve( state: headline.state, noun: headline.noun, pronoun: headline.person.possessive, place: headline.place ) case HeadlineType.DontWant rtn: say.dontWantYouToKnow(headline.noun, headline.otherNoun) case HeadlineType.GiftIdeas rtn: say.giftIdeas(headline.number, headline.noun, headline.state) case HeadlineType.ReasonsWhy rtn: say.reasonsWhy(headline.number, headline.noun, headline.otherNumber) case HeadlineType.JobAutomated rtn: say.jobAutomated( state: headline.state, noun: headline.noun, pronoun1: headline.person.possessive, pronoun2: headline.person.personal, verb: headline.person.pastOfBe ) default rtn: String() override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the word lists and the draw, with no input or output. The word lists are data //the game draws from, as the book's lists are. people() as pure <- rtn as List of Pronouns: [ Pronouns(objective: "Her", possessive: "Her", personal: "She", pastOfBe: "Was"), Pronouns(objective: "Him", possessive: "His", personal: "He", pastOfBe: "Was"), Pronouns(objective: "Them", possessive: "Their", personal: "They", pastOfBe: "Were") ] states() as pure <- rtn as List of String: [ "California", "Texas", "Florida", "New York", "Pennsylvania", "Illinois", "Ohio", "Georgia", "North Carolina", "Michigan" ] nouns() as pure <- rtn as List of String: [ "Athlete", "Clown", "Shovel", "Paleo Diet", "Doctor", "Parent", "Cat", "Dog", "Chicken", "Robot", "Video Game", "Avocado", "Plastic Straw", "Serial Killer", "Telephone Psychic" ] places() as pure <- rtn as List of String: [ "House", "Attic", "Bank Deposit Box", "School", "Basement", "Workplace", "Donut Shop", "Apocalypse Bunker" ] //`when` is an EK9 keyword, so the original's WHEN list is named timings. timings() as pure <- rtn as List of String: ["Soon", "This Year", "Later Today", "RIGHT NOW", "Next Week"] websites() as pure <- rtn as List of String: ["wobsite", "blag", "Facebuuk", "Googles", "Facesbook", "Tweedie", "Pastagram"] //Integer(text) is unset when the text is not a number, so the parse is the validation. countFrom() as pure -> reply as String <- rtn as Integer: Integer(reply.trim()) pick() -> items as List of String rng as Random <- rtn as String: items.getOrDefault(rng.next(0, length items - 1), String()) pickPerson() -> rng as Random <- rtn as Pronouns: Pronouns() everyone <- people() rtn: everyone.getOrDefault(rng.next(0, length everyone - 1), Pronouns()) pickPlural() -> items as List of String rng as Random <- rtn as String: pick(items, rng) + "s" //The headline types come from the enumeration itself, so adding a type adds it to the draw. pickHeadlineType() -> rng as Random <- rtn as HeadlineType: HeadlineType() kinds <- cat HeadlineType | collect as List of HeadlineType rtn: kinds.getOrDefault(rng.next(0, length kinds - 1), HeadlineType()) //The kind is drawn first, then the blanks that kind fills, in the order its template //reads them. drawnHeadline() -> rng as Random <- rtn as Headline: Headline() rtn.kind: pickHeadlineType(rng) switch rtn.kind case HeadlineType.Millennials rtn.noun: pick(nouns(), rng) case HeadlineType.WithoutThis rtn.noun: pick(nouns(), rng) rtn.otherNoun: pickPlural(nouns(), rng) rtn.timing: pick(timings(), rng) case HeadlineType.BigCompanies rtn.person: pickPerson(rng) rtn.state: pick(states(), rng) rtn.noun: pick(nouns(), rng) rtn.otherNoun: pick(nouns(), rng) case HeadlineType.YouWontBelieve rtn.state: pick(states(), rng) rtn.noun: pick(nouns(), rng) rtn.person: pickPerson(rng) rtn.place: pick(places(), rng) case HeadlineType.DontWant rtn.noun: pickPlural(nouns(), rng) rtn.otherNoun: pickPlural(nouns(), rng) case HeadlineType.GiftIdeas rtn.number: rng.next(FewestGiftIdeas, MostGiftIdeas) rtn.noun: pick(nouns(), rng) rtn.state: pick(states(), rng) case HeadlineType.ReasonsWhy rtn.number: rng.next(FewestReasons, MostReasons) rtn.noun: pickPlural(nouns(), rng) rtn.otherNumber: rng.next(1, rtn.number) case HeadlineType.JobAutomated rtn.person: pickPerson(rng) rtn.state: pick(states(), rng) rtn.noun: pick(nouns(), rng) default rtn: Headline() defines function //THE source of randomness, copied unchanged from lib/RandomSource.ek9: SystemRandom(), or //SeededRandom(seed) when BIGBOOK_SEED holds a number (run-and-diff.sh sets it from seed.txt). randomSource() <- rtn as Random: SeededRandom(Integer(EnvVars().get("BIGBOOK_SEED"))) ?: SystemRandom() //THE language of the words, copied unchanged from lib/Language.ek9: BIGBOOK_LANG when it //is set (fr, zh, ...), otherwise en. language() <- rtn as String: EnvVars().get("BIGBOOK_LANG") ?: "en" //THE WORDS: every word the player sees - the eight headline templates among them - apart //from the word lists the game draws the blanks from. defines text for "en" Say introduction() `By Al Sweigart al@inventwithpython.com Our website needs to trick people into looking at ads!` askCount() "Enter the number of clickbait headlines to generate:" notANumber() "Please enter a number." areMillennialsKilling() -> noun as String `Are Millennials Killing the ${noun} Industry?` withoutThis() -> noun as String pluralNoun as String timing as String `Without This ${noun}, ${pluralNoun} Could Kill You ${timing}` bigCompaniesHate() -> pronoun as String state as String noun1 as String noun2 as String `Big Companies Hate ${pronoun}! See How This ${state} ${noun1} Invented a Cheaper ${noun2}` youWontBelieve() -> state as String noun as String pronoun as String place as String `You Won't Believe What This ${state} ${noun} Found in ${pronoun} ${place}` dontWantYouToKnow() -> pluralNoun1 as String pluralNoun2 as String `What ${pluralNoun1} Don't Want You To Know About ${pluralNoun2}` giftIdeas() -> number as Integer noun as String state as String `${number} Gift Ideas to Give Your ${noun} From ${state}` reasonsWhy() -> number1 as Integer pluralNoun as String number2 as Integer `${number1} Reasons Why ${pluralNoun} Are More Interesting Than You Think (Number ${number2} Will Surprise You!)` jobAutomated() -> state as String noun as String pronoun1 as String pronoun2 as String verb as String `This ${state} ${noun} Didn't Think Robots Would Take ${pronoun1} Job. ${pronoun2} ${verb} Wrong.` postThese() -> website as String timing as String `Post these to our ${website} ${timing} or you're fired!` defines program //Clickbait Headline Generator (Big Book project 11). Makes up as many clickbait headlines as //the player asks for. The game decides; the Console asks and tells, in the player's words. //The golden replays a seeded game (seed.txt). Clickbait() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() if count <- console.nextCount() made <- 0 while made < count console.showHeadline(drawnHeadline(rng)) made += 1 website <- pick(websites(), rng) timing <- pick(timings(), rng) console.showSignOff(website, timing)
How the port works
The original builds each headline in the same lines that choose its random blanks and print
it. The port keeps three things apart: the game draws a headline, the Console asks for the
count and shows each headline, and Say holds every word.
The game draws. The word lists — states, nouns, places, timings, websites, people — are
data the game draws the blanks from, as the book's lists are. drawnHeadline(rng) draws a
kind, then the blanks that kind fills, in the order its template reads them, and returns a
Headline record: the kind and its blanks, the value and never the words for it. A kind uses
only some of the record's blanks; the others stay unset, which does no harm, because each
template is handed only its own. HeadlineType is an enumeration of the eight kinds,
replacing a numbered 1-to-8 choice, and the list it is drawn from is derived from the type
itself, cat HeadlineType | collect as List of HeadlineType, so a ninth headline type added
to the enumeration joins the random draw automatically — there is no second list that could
drift out of step with it.
The original's three parallel word lists — one each for a pronoun's objective, possessive and
personal form, plus a separate check for whether the past tense of "to be" should read "Was"
or "Were" — become one Pronouns record per person, built with a pure, assign-if-unset
(:=?) constructor. "Her", "She" and "Was" (or "Their", "They" and "Were") travel together as
one object picked by one random index, so there is no way for a slipped index to pair
"Their" with "He".
The Console asks and tells. nextCount() uses the same unset-guard idiom as the other
ports in this book, while not rtn? and not input.ended(), reading an unset Integer for
"not a number yet" rather than any other sentinel. headlineText words a drawn headline with
the template for its kind: a switch over HeadlineType that must name every one of the
eight values and still carry a default — two separate rules the compiler enforces, so a
ninth case genuinely cannot be forgotten. EK9's switch has no fallthrough to guard against
in the first place, so each case simply stands alone, much like a chain of Python elifs.
Say holds the words. The eight headline templates are methods on a text construct,
each an interpolated template such as `Are Millennials Killing the ${noun} Industry?`,
beside the introduction, the prompts and the sign-off. This is EK9's dedicated construct for
locale-specific, parameterised strings: a second language would be a second
defines text for "..." block alongside this one, and the compiler checks that every locale
of the same text declares the same methods — there is no risk of an English-only template
quietly going untranslated. Several templates take four or more arguments, and from four
arguments up EK9 requires every call to name them —
say.jobAutomated(state: ..., noun: ..., pronoun1: ..., pronoun2: ..., verb: ...) rather
than five bare positions — which is a compiler-enforced readability rule, not a style choice.
The program is the flow: the introduction, the count, one drawn headline after another,
then the sign-off with a drawn website and timing. It chooses the words with
Say(language()) ?: Say("en"), so a language with no words of its own falls back to English.
when turns out to be a reserved EK9 keyword — it cannot even be used as a variable name —
so the original's WHEN word list is named timings here instead.