ToCode
1.43K subscribers
3.42K links
טיפים קצרים למתכנתים מאת ינון פרק
Download Telegram
מה שעשינו פעם קודמת
אחת ההטיות שקשה להתגבר עליהן היא הטיית ה"פעם קודמת עשינו את זה". במיוחד אם פעם קודמת הגענו לתוצאה סבירה.

היתרון הגדול בלבחור בפיתרון שבחרנו בו פעם קודמת הוא שזה חוסך מחשבה. צריכים דף נחיתה? יש לי בחור שעושה דפי נחיתה ופעם קודמת הוא עשה לי דף מצוין. מתלבטים בין React ל Vue? פעם קודמת בחרנו ריאקט וזה עבד מצוין. לא צריך לחשוב פעם שניה על משהו שכבר חשבנו עליו.

אבל אני לא בטוח שזה כל כך פשוט. הנה עוד כמה נקודות למחשבה:

1. אולי פעם קודמת התוצאה היתה סבירה אבל היתה יכולה להיות הרבה יותר טובה עם האלטרנטיבה.

2. אולי דברים השתנו מאז הפעם הקודמת להיום. אולי יש כלים יותר טובים שפעם קודמת לא היו.

3. אולי אתם השתניתם מאז הפעם הקודמת. אולי מה שעבד בשבילכם אז כבר לא יעבוד היום. אולי חלק מהאילוצים שהיו לכם אז כבר לא קיימים ובלעדיהם אפשר למצוא פיתרון טוב יותר.

כשאנחנו מוכנים לבחון מחדש החלטות שעשינו אנחנו מפרידים את עצמנו מההחלטה. אני כבר לא "מתכנת ריאקט" או "מתכנתת Java". אני פה בשביל לפתור בעיות ואני אשמח לבחור את הדרך הטובה ביותר לפתור את הבעיה הבאה.
👍6
מה המילה שמופיעה הכי הרבה פעמים במובי דיק?
לא צריך יותר מכמה שורות פייתון וספר חופשי או שניים כדי למצוא מילים מעניינות באנגלית. במקרה של מובי דיק החיים קלים כי הספר ללא זכויות יוצרים ואפשר למצוא את כל הטקסט המקורי בפרויקט גוטנברג בקישור:

https://www.gutenberg.org/cache/epub/2701/pg2701.txt

עכשיו בואו נלך לקרוא אותו, אבל בהילוך מהיר.

איך לשבור טקסט למילים
שלב חשוב ראשון בשביל למצוא מילים מעניינות בטקסט יהיה לשבור את הטקסט למילים. אני יודע שאתם חושבים על איזה split אבל עם מילים זה טיפה יותר מסובך - אין לי מה לעשות עם שמות של אנשים או מקומות, ובאופן כללי מילים מעניינות יהיו בדרך כלל פעלים, שמות עצם או תארים. אני גם לא רוצה סימני פיסוק שיפריעו לחיפוש. אז כן אפשר למחוק תווים מיותרים אבל יותר קל לתת למחשב לעשות את זה.

ספריית spacy יודעת לחלק טקסט לטוקנים (כלומר מילים או סימני פיסוק), וגם להגיד מה התפקיד של כל טוקן בטקסט.

הקוד הזה לדוגמה ייקח משפט וידפיס את כל הטוקנים והתפקיד של כל טוקן במשפט:

import spacy

nlp = spacy.load("en_core_web_trf")
doc = nlp("This is such a long sentence that I cannot read it so go on please.")

print([(w.text, w.pos_) for w in doc])


והפלט:

[('This', 'PRON'), ('is', 'AUX'), ('such', 'DET'), ('a', 'DET'), ('long', 'ADJ'), ('sentence', 'NOUN'), ('that', 'SCONJ'), ('I', 'PRON'), ('can', 'AUX'), ('not', 'PART'), (
'read', 'VERB'), ('it', 'PRON'), ('so', 'CCONJ'), ('go', 'VERB'), ('on', 'ADP'), ('please', 'INTJ'), ('.', 'PUNCT')]


עכשיו שיש לי את חלקי הדיבור אפשר לסנן ולהדפיס רק את המילים המעניינות - כלומר הפעלים, התארים ושמות העצם.

בעיה 1 - מאיפה משיגים את הטקסט
הטקסט של מובי דיק זמין אונליין ולכן בשביל לקבל אותו אני יכול להשתמש בקוד פייתון הבא:

import urllib.request
import ssl

if __name__ == "__main__":
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

with urllib.request.urlopen("https://www.gutenberg.org/cache/epub/2701/pg2701.txt",
context=ctx) as f:
text = f.read().decode('utf8')
print(len(text))


בקוד שלי התעלמתי מבעיות ב SSL. אני חושב שהבעיות נגרמו בגלל בעיית התקנה או קבצים חסרים אצלי על המחשב, אבל בכל מקרה לתוכנית הדוגמה של מובי דיק זה לא היה חשוב.

בעיה 2 - הטקסט ארוך מדי
ניסיון לחבר בין שתי תוכניות הדוגמה נכשל בגלל שהטקסט של מובי דיק ארוך מדי. לכן הוספתי עוד פונקציה ששוברת טקסט ארוך לקטעים קטנים יותר, בלי לשבור מילים באמצע:

def chunks(text: str, max_size: int):
while len(text) > max_size:
space_index = text[:max_size].rfind(' ')
yield text[:space_index]
text = text[space_index:]
print(f"{len(text)} chars left")
yield text


עכשיו אנחנו מוכנים לספור את המילים:

def count_words(text: str):
nlp = spacy.load("en_core_web_trf")
word_count = Counter()
for chunk in chunks(text, 100_000):
doc = nlp(chunk)
word_tags = {'ADV', 'VERB', 'NOUN', 'ADJ'}
weird_tokens = {"'s", "so", "then", "there"}
word_count.update([w.text.lower()
for w in doc
if (w.pos_ in word_tags) and (w.text not in weird_tokens)])

return word_count




התוכנית המלאה ורשימות מילים מעניינות
אז איזה מילים יש במובי דיק? אני רציתי למצוא 3 רשימות. הרשימה הראשונה היא של המילים הכי נפוצות. המילה הכי נפוצה בספר למקרה שתהיתם היא כמובן לוויתן. אבל יש עוד כמה מילים נפוצות שאנחנו כנראה מכירים. הרשימה השניה היתה של המילים הכי פחות נפוצות בספר, והרשימה השלישית שנראתה לי הכי מעניינת היתה של ה 200 מילים הכי פחות נפוצות אבל שהופיעו יותר מפעם אחת.

סך הכל זאת היתה כל התוכנית:

import spacy
from collections import Counter
import urllib.request
import ssl

def chunks(text: str, max_size: int):
while len(text) > max_size:
space_index = text[:max_size].rfind(' ')
yield text[:space_index]
text = text[space_index:]
print(f"{len(text)} chars left")
yield text
1
def count_words(text: str):
nlp = spacy.load("en_core_web_sm")
word_count = Counter()
for chunk in chunks(text, 100_000):
doc = nlp(chunk)
word_tags = {'ADV', 'VERB', 'NOUN', 'ADJ'}
weird_tokens = {"'s", "so", "then", "there"}
word_count.update([w.text.lower()
for w in doc
if (w.pos_ in word_tags) and (w.text not in weird_tokens)])

return word_count


if __name__ == "__main__":
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

with urllib.request.urlopen("https://www.gutenberg.org/cache/epub/2701/pg2701.txt",
context=ctx) as f:
text = f.read().decode('utf8')
word_count = count_words(text)
print(f"Book has {len(word_count)} words")

print("--- most common 200 words:")
print(word_count.most_common(200))

print("--- least common 200 words:")
print(word_count.most_common()[:-201:-1])

print("--- least common 200 words that appear more than once:")
word_count_greater_than_1 = sorted({k: v for k, v in word_count.items() if v > 1}.items(),
key=lambda x: x[1])

print(word_count_greater_than_1[:200])


תוצאות? בטח. רשימה ראשונה - המילים הכי נפוצות בספר מובי דיק:
[('whale', 894), ('now', 781), ('ship', 515), ('more', 507), ('man', 504), ('old', 440), ('other', 432), ('sea', 431), ('’s', 416), ('only', 378), ('head', 333), ('boat', 331), ('time', 329), ('long', 327), ('very', 322), ('here', 316), ('ye', 315), ('still', 311), ('great', 300), ('said', 296), ('most', 286), ('seemed', 279), ('last', 275), ('way', 269), ('chapter', 267), ('see', 265), ('again', 258), ('have', 256), ('yet', 247), ('whales', 246), ('little', 246), ('_', 243), ('men', 239), ('say', 233), ('round', 230), ('first', 225), ('much', 223), ('same', 213), ('such', 208), ('hand', 207), ('side', 206), ('never', 206), ('ever', 205), ('own', 205), ('good', 202), ('look', 200), ('almost', 196), ('even', 192), ('go', 192), ('deck', 188), ('thing', 187), ('water', 186), ('all', 185), ('as', 183), ('too', 182), ('made', 177), ('come', 177), ('away', 175), ('world', 174), ('white', 174), ('day', 171), ('thou', 170), ('life', 167), ('far', 165), ('seen', 164), ('do', 163), ('many', 161), ('well', 159), ('line', 158), ('let', 157), ('eyes', 156), ('had', 156), ('fish', 154), ('part', 153), ('sort', 152), ('cried', 150), ('thought', 148), ('know', 148), ('back', 147), ('once', 147), ('night', 147), ('boats', 145), ('so', 144), ('air', 140), ('crew', 137), ('whole', 136), ('full', 135), ('take', 134), ('thus', 134), ('things', 133), ('tell', 133), ('small', 130), ('soon', 129), ('feet', 127), ('hands', 125), ('came', 123), ('whaling', 122), ('mast', 121), ('has', 121), ('captain', 119), ('think', 118), ('half', 118), ('found', 117), ('just', 117), ('place', 117), ('called', 116), ('make', 114), ('saw', 112), ('times', 112), ('right', 110), ('body', 110), ('work', 110), ('poor', 108), ('high', 106), ('heard', 106), ('moment', 105), ('sight', 104), ('sperm', 104), ('end', 102), ('aye', 101), ('stand', 100), ('one', 100), ('sail', 98), ('strange', 98), ('hold', 98), ('years', 96), ('however', 95), ('face', 95), ('sun', 95), ('down', 94), ('voyage', 94), ('few', 94), ('went', 94), ('also', 93), ('dead', 93), ('get', 92), ('certain', 91), ('is', 90), ('oil', 90), ('going', 89), ('heart', 89), ('perhaps', 89), ('stood', 89), ('indeed', 89), ('give', 88), ('ships', 88), ('eye', 87), ('sometimes', 87), ('heads', 86), ('days', 86), ('seems', 86), ('like', 86), ('true', 85), ('matter', 85), ('arm', 85), ('iron', 85), ('hard', 84), ('set', 84), ('black', 83), ('soul', 82), ('death', 81), ('seem', 81), ('wild', 81), ('standing', 81), ('cabin', 81), ('known', 80), ('tail', 80), ('always', 80), ('present', 80), ('seas', 79), ('large', 79), ('mind', 79), ('young', 79), ('light', 79), ('length', 78), ('land', 78), ('instant', 77), ('least', 76), ('open', 76), ('harpooneer', 76), ('enough', 76), ('bed', 76), ('at', 76), ('fire', 75), ('mate', 75), ('harpoon', 75), ('leg', 75), ('word', 74), ('morning', 74), ('vast', 73), ('living', 73), ('board', 73), ('put', 73), ('did', 73), ('lay', 73), ('done', 73), ('often', 73), ('-', 72), ('point', 71), ('deep', 70)]


רשימה שניה - המילים הכי פחות נפוצות בספר מובי דיק:
[('newsletter', 1), ('subscribe', 1), ('includes', 1), ('confirmed', 1), ('volunteer', 1), ('network', 1), ('originator', 1), ('checks', 1), ('addresses', 1), ('donation', 1), ('web', 1), ('treatment', 1), ('gratefully', 1), ('international', 1), ('donors', 1), ('accepting', 1), ('prohibition', 1), ('solicitation', 1), ('www.gutenberg.org/donate', 1), ('locations', 1), ('paperwork', 1), ('charities', 1), ('outdated', 1), ('widespread', 1), ('www.gutenberg.org/contact', 1), ('deductible', 1), ('identification', 1), ('corporation', 1), ('educational', 1), ('501(c)(3', 1), ('sections', 1), ('ensuring', 1), ('goals', 1), ('financial', 1), ('formats', 1), ('synonymous', 1), ('c', 1), ('deletions', 1), ('additions', 1), ('modification', 1), ('alteration', 1), ('employee', 1), ('indemnify', 1), ('provisions', 1), ('void', 1), ('unenforceability', 1), ('invalidity', 1), ('maximum', 1), ('violates', 1), ('types', 1), ('implied', 1), ('disclaimers', 1), ('elect', 1), ('distributor', 1), ('1.f.3', 1), ('warranty', 1), ('remedies', 1), ('disclaim', 1), ('codes', 1), ('virus', 1), ('disk', 1), ('infringement', 1), ('transcription', 1), ('data', 1), ('inaccurate', 1), ('defects', 1), ('stored', 1), ('proofread', 1), ('expend', 1), ('employees', 1), ('manager', 1), ('discontinue', 1), ('notifies', 1), ('periodic', 1), ('legally', 1), ('owed', 1), ('taxes', 1), ('%', 1), ('exporting', 1), ('hypertext', 1), ('processing', 1), ('proprietary', 1), ('nonproprietary', 1), ('compressed', 1), ('binary', 1), ('redistribute', 1), ('detach', 1), ('unlink', 1), ('redistributing', 1), ('indicating', 1), ('texts', 1), ('accessed', 1), ('1.e.', 1), ('representations', 1), ('downloading', 1), ('govern', 1), ('unprotected', 1), ('compilation', 1), ('1.e', 1), ('1.c', 1), ('indicate', 1), ('1.a.', 1), ('renamed', 1), ('orphan', 1), ('retracing', 1), ('sheathed', 1), ('padlocks', 1), ('dirgelike', 1), ('liberated', 1), ('ixion', 1), ('suction', 1), ('halfspent', 1), ('forth?—because', 1), ('thrill', 1), ('etherial', 1), ('intercept', 1), ('incommoding', 1), ('tauntingly', 1), ('backwardly', 1), ('touched;—at', 1), ('coincidings', 1), ('ironical', 1), ('intermixingly', 1), ('whelmings', 1), ('inanimate', 1), ('animate', 1), ('lookouts', 1), ('infatuation', 1), ('gaseous', 1), ('mediums', 1), ('bewildering', 1), ('bowstring', 1), ('mutes', 1), ('voicelessly', 1), ('grooves;—ran', 1), ('grapple', 1), ('unconquering', 1), ('comber', 1), ('foregone', 1), ('prow,—death', 1), ('bullied', 1), ('uncracked', 1), ('unsurrendered', 1), ('flume', 1), ('dislodged', 1), ('buttress', 1), ('predestinating', 1), ('inactive', 1), ('coppers', 1), ('though;—cherries', 1), ('gulping', 1), ('assassins', 1), ('brushwood', 1), ('mattrass', 1), ('unwinking', 1), ('unappeasable', 1), ('fidelities', 1), ('plaid', 1), ('gap', 1), ('splashing', 1), ('persecutions', 1), ('evolution', 1), ('crashing', 1), ('cracks!—’tis', 1), ('sinew', 1), ('tug', 1), ('ungraduated', 1), ('unprepared', 1), ('foreknew', 1), ('writhed', 1), ('fiercer', 1), ('tell”—he', 1), ('rowlocks', 1), ('pertinaciously', 1), ('abate', 1), ('busying', 1), ('staved', 1), ('judicious', 1), ('seekest', 1), ('again.—aye', 1), ('breath—“aye', 1), ('befooled!”—drawing', 1), ('befooled', 1), ('frayed', 1), ('flailed', 1), ('knitted', 1), ('tiers', 1), ('combinedly', 1), ('creamed', 1), ('brokenly', 1), ('swamping', 1), ('bedraggled', 1), ('berg', 1), ('upheaved', 1), ('ahab!—shudder', 1), ('it!—where', 1), ('soars', 1), ('vane”—pointing', 1), ('again!—drive', 1), ('whale!—ho', 1)]


רשימה שלישית - המילים הכי פחות נפוצות אבל שעדיין מופיעות יותר מפעם אחת:
[('restrictions', 2), ('updated', 2), ('*', 2), ('dick', 2), ('loomings', 2), ('postscript', 2), ('historically', 2), ('diminish?—will', 2), ('aloft.—thunder', 2), ('chase.—third', 2), ('combination', 2), ('defunct', 2), ('indebted', 2), ('dusting', 2), ('grammars', 2), ('vaulted', 2), ('entertaining', 2), ('affording', 2), ('rosy', 2), ('sadness', 2), ('tuileries', 2), ('gulp', 2), ('hoary', 2), ('paunch', 2), ('biggest', 2), ('verbal', 2), ('gulf', 2), ('insomuch', 2), ('parmacetti', 2), ('boil', 2), ('quid', 2), ('pikes', 2), ('fry', 2), ('troops', 2), ('caution', 2), ('discoverer', 2), ('fence', 2), ('abode', 2), ('conceal', 2), ('boldness', 2), ('revenue', 2), ('momentary', 2), ('serves', 2), ('impetus', 2), ('enemies', 2), ('swords', 2), ('finny', 2), ('mightier', 2), ('flounders', 2), ('gateway', 2), ('monument', 2), ('sprout', 2), ('resounds', 2), ('rushes', 2), ('neglected', 2), ('opportunities', 2), ('witnessing', 2), ('formidable', 2), ('displays', 2), ('employ', 2), ('a.d.', 2), ('inevitable', 2), ('national', 2), ('rebounds', 2), ('totally', 2), ('ex', 2), ('arches', 2), ('entrances', 2), ('alcoves', 2), ('whites', 2), ('manage', 2), ('cheery', 2), ('giant', 2), ('regulating', 2), ('warehouses', 2), ('surrounds', 2), ('waterward', 2), ('battery', 2), ('cooled', 2), ('seaward', 2), ('pent', 2), ('benches', 2), ('extremest', 2), ('suffice', 2), ('caravan', 2), ('metaphysical', 2), ('employs', 2), ('hermit', 2), ('woodlands', 2), ('overlapping', 2), ('spurs', 2), ('bathed', 2), ('sighs', 2), ('shepherd', 2), ('wade', 2), ('lilies', 2), ('cataract', 2), ('poet', 2), ('pedestrian', 2), ('deity', 2), ('tormenting', 2), ('rag', 2), ('tribulations', 2), ('judiciously', 2), ('mummies', 2), ('grasshopper', 2), ('touches', 2), ('indignity', 2), ('orchard', 2), ('thieves', 2), ('entailed', 2), ('cheerfully', 2), ('wholesome', 2), ('leaders', 2), ('secretly', 2), ('magnificent', 2), ('tragedies', 2), ('delusion', 2), ('inducements', 2), ('gates', 2), ('inmost', 2), ('amazingly', 2), ('monopolising', 2), ('sally', 2), ('concernment', 2), ('grapnels', 2), ('expensive', 2), ('congealed', 2), ('tinkling', 2), ('building', 2), ('stumble', 2), ('porch', 2), ('dilapidated', 2), ('carted', 2), ('ruins', 2), ('cheap', 2), ('pea', 2), ('palsied', 2), ('judging', 2), ('writer', 2), ('lookest', 2), ('improvements', 2), ('copestone', 2), ('curbstone', 2), ('tatters', 2), ('drinks', 2), ('tepid', 2), ('frosted', 2), ('thoroughly', 2), ('diligent', 2), ('systematic', 2), ('contemplation', 2), ('oft', 2), ('unwarranted', 2), ('yeast', 2), ('sublimity', 2), ('deceptive', 2), ('combat', 2), ('hyperborean', 2), ('yielded', 2), ('aggregated', 2), ('opinions', 2), ('dismantled', 2), ('purposing', 2), ('sickle', 2), ('segment', 2), ('mown', 2), ('mower', 2), ('wondered', 2), ('imbedded', 2), ('decanters', 2), ('bottles', 2), ('withered', 2), ('dearly', 2), ('sells', 2), ('pours', 2), ('villanous', 2), ('cheating', 2), ('rudely', 2), ('surround', 2), ('skrimshander', 2), ('objections', 2), ('liked', 2), ('adjoining', 2), ('nightmare', 2), ('complexioned', 2), ('seed', 2), ('coats', 2), ('comforters', 2), ('icicles', 2), ('molasses', 2), ('sovereign', 2), ('capering', 2), ('interested', 2), ('ordained', 2), ('partner', 2), ('brawn', 2), ('reminiscences', 2), ('stature', 2), ('orgies', 2)]
👍2
רק מרגיש ככה
"הקוד הזה פח, אי אפשר לתחזק אותו חייבים לזרוק הכל ולכתוב מחדש. אין בריה."

"שמע אני לא מבין על מה משלמים לנו בכלל. המודל העסקי של הסטארט-אפ הזה לא הגיוני והמוצר לא עובד. נס שהמשקיעים ממשיכים לשלם."

"תקשיבי ה Python הזה לא יתפוס. אין מצב שאנשים יעזבו את פרל בשביל שפה כל כך משעממת."

"אני לא מאמינה שהם לא התעניינו בפרויקט, איך הלכה חצי שנה. חבל שבכלל נכנסתי לזה, עדיף היה למצוא עבודה אמיתית."

"הפעם זה בטוח יצליח. בטוח."

לפעמים כשהברך כואבת זה בגלל שהולך לרדת גשם. לפעמים היא סתם כואבת. ולפעמים יש גשם בלי קשר לתחושה. מותר להרגיש כל דבר. חשוב להבדיל בין התחושה לדבר האמיתי.

"אני מרגיש שהקוד לא טוב. אלה הדברים שלדעתי לא עובדים בו. אלה הסיבות בגללן הקוד נכתב ככה אבל לדעתי סיבות אלה כבר לא רלוונטיות. אלה הדברים שאני חושב שכדאי לתקן ובסדר הזה. אגב אלה הדברים שכן אהבתי בקוד."

תבואו עם נתונים ותתרגלו להקשיב לאנשים שבאים עם נתונים. זה אולי פחות כיף אבל הרבה יותר פרודוקטיבי.
🔥2
ואם אין before בספריית הבדיקה שלך?
בימים אלה אני בונה מחדש את קורס node.js שבאתר. הגירסה החדשה תכיל המון TypeScript ותכסה בנוסף ל node גם את Deno ו Bun והמטרה שלי היא שרוב הקורס יעבוד בכל שלושת סביבות הריצה.

בגדול המצב של TypeScript בצד שרת הוא מאוד טוב וגם דינו וגם באן מספיק בשלים בשביל לכתוב עליהם, אבל מדי פעם יש שטויות ובעיות תאימות. דוגמה קטנה היא שכשטוענים את המודול test של node מתוך deno אין תמיכה ב before.

ומה אם בכל זאת אנחנו רוצים להריץ קוד לפני בדיקה? נו, תמיד אפשר להיות יצירתיים. בדוגמה מהקורס רציתי להריץ קוד שמאתחל טבלה בבסיס נתונים בזיכרון לפני שאני מריץ קוד. בהשראה מ pytest כתבתי במקום before את הפונקציה הבאה:

import { Database } from '@/db_types.ts'
import { Kysely } from 'kysely'
import { DenoSqliteDialect } from "@soapbox/kysely-deno-sqlite";
import { DB as Sqlite } from 'https://deno.land/x/sqlite/mod.ts';

export const useDB = async (test: (db: Kysely<Database>) => Promise<void>) => {
const _db = new Kysely<Database>({
dialect: new DenoSqliteDialect({
database: new Sqlite(':memory:'),
}),
});

await _db.schema
.createTable('contact_info')
.addColumn('id', 'integer', (col) => col.primaryKey())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', col => col.unique())
.execute()

try {
await test(_db);
} finally {
await _db.destroy();
}
}


עכשיו הבדיקה צריכה רק להפעיל את הפונקציה והיא מקבלת אוטומטית גם את קוד האיתחול וגם את קוד הניקוי. זה נראה ככה:

test('POST /contacts created a new contact', async () => {
await useDB(async db => {
await superdeno(app(db))
.post('/api/v1/contacts')
.set('Accept', 'application/json')
.send({name: "a", email: "a@gmail.com"})
.expect(200);

const res = await superdeno(app(db))
.get('/api/v1/contacts')
.set('Accept', 'application/json')

assert.deepEqual([
{ id: 1, name: "a", email: "a@gmail.com" }
], res.body);
})
});



וכן המחשבה הראשונה שלי היתה שהחיים היו מושלמים אם דברים היו עובדים בכל הסביבות. אבל במחשבה שניה אני חושב שללמוד להסתדר גם כשדברים לא עובדים כמו בספר זו גם מיומנות חשובה ששווה להראות בקורס.
בגדול זה עובד
ג'ואי צ'נג (אני מקווה שאני כותב את השם הזה נכון) עשתה עבודה מטורפת כדי לאפשר ל node.js לטעון עם require מודולים של ESM. היא כתבה על זה בבלוג שלה כאן:

https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/

אין ספק שהאופן שבו אנחנו כותבים TypeScript ב node.js הוא עקום הרבה בגלל הסיפור הזה. בשביל שדברים יעבדו כמו שצריך ברוב הפרויקטים אנחנו כותבים קוד TypeScript שנראה כמו ESM, אבל אז מקמפלים אותו ל CJS בשביל ש node יריץ אותו, וזה בגדול עובד עד שמנסים לעשות דברים שיש רק ב ESM ואז הכל נשבר.

בקיצור ג'ואי צ'נג כתבה PR שמאפשר לקוד CJS לעשות require לקוד ESM, שזה כבר מאוד משפר את המצב להרבה מצבים. אבל זה עדיין עקום כי זה לא מטפל בבעיה האמיתית, שהיא הקומפילציה ל CJS רק בשביל שדברים יעבדו כמו שצריך עם מודולים ישנים ב npm.

(כי אם הכל היה ESM לא היינו צריכים לטעון ESM עם require).

אבל הנקודה הכי חשובה כאן היא מה לא עובד - למרות כל העבודה, למרות שהיא הצליחה לפתור הרבה בעיות לאנשים, היא עדיין השאיר נקודה פתוחה - ה await מחוץ לכל פונקציה לא יעבוד.

ופה יש התלבטות אמיתית שאנחנו מוצאים בהרבה מערכות וספריות וזה מעניין לראות איך הדברים האלה נוצרים. מי שכותבת את הקוד יודעת שיש לה מקרה שהיא לא רוצה לטפל בו, כי הוא מסובך ומחוץ לסקופ, אז היא מייצרת פיתרון שעובד רק ל 90% מהמקרים. שנתיים אחר כך מישהו מגיע למערכת הזאת ומנסה להריץ את אותם מקרים שלא נתמכים ולא מבין - איך זה לא עובד? הרי בכל התיעוד כתוב שאפשר לעשות XYZ. מה מיוחד באיך שאני עושה את זה? למה רק אצלי זה לא עובד?

במקרים כאלה התקשורת היא הכל. קוד שמטפל ב 90% מהמקרים צריך לזהות את ה 10% הנותרים ולהציג הודעת שגיאה מאוד מפורטת עדיף עם קישור להסבר מה מיוחד בדבר שניסית לעשות ולמה החלטנו לא לתמוך בזה. אין דבר יותר מתסכל מלשבת יומיים רק בשביל לגלות שהמקרה שלך נפל מחוץ לסקופ.
שב רגע בצד טייפסקריפט, אני צריך לעבוד
נתבונן בשתי פונקציות בטייפסקריפט שמשתמשות במערכת הטיפוסים של קיסלי עבור גישה לבסיס נתונים SQL:

async editNote(username: string, noteId: number, newText: string) {
const user = await db.selectFrom('users').selectAll().where('users.name', '=', username).executeTakeFirstOrThrow();

return db
.updateTable('notes')
.set('text', newText)
.where(noteBelongsToUser(user.id, noteId))
.returningAll()
.executeTakeFirstOrThrow()
},

async deleteNote(username: string, noteId: number) {
const user = await db.selectFrom('users').selectAll().where('users.name', '=', username).executeTakeFirstOrThrow();

return db
.deleteFrom('notes')
.where(noteBelongsToUser(user.id, noteId))
.returningAll()
.executeTakeFirstOrThrow()
},


רואים את הדמיון? ברור שכן. שתיהן מוציאות שאילתה ראשונה כדי לקבל את המשתמש, ואז בונות שאילתה נוספת בשביל לעשות משהו עם המשתמש - פעם אחת למחוק מידע ופעם שניה לעדכן מידע.

ניסיון לאחד אותן ולבטל את הקוד המשותף עשוי להיראות כך:

async function dry(db: Kysely<Database>, 
username: string,
noteId: number,
f: (db: Kysely<Database>) => ???) {
const user = await db.selectFrom('users').selectAll().where('users.name', '=', username).executeTakeFirstOrThrow();

return f(db)
.where(noteBelongsToUser(user.id, noteId))
.returningAll()
.executeTakeFirstOrThrow()
}


הקוד הזה עובד ואפשר להשתמש בו בקלות למשל:

async easyDeleteNote(username: string, noteId: number) {
dry(db, username, noteId, (db) => db.deleteFrom('notes'))
}


יש רק בעיה אחת, סימן אחד שחסר לי - מה מחזירה הפונקציה f ? מה לכתוב במקום סימני השאלה?

בעולם מתוקן טייפסקריפט היה מזהה שאני משתמש רק בחלקים משותפים מבין שני הממשקים שהפונקציות מחזירות ומאפשר לי לכתוב איחוד של הטיפוסים או אולי אפילו מבין את זה לבד. בעולם שלנו זה עוד אחד מהמצבים בהם הבאנו את טייפסקריפט לקצה.

עכשיו צריך לבחור, האם להתעקש על מערכת הטיפוסים של טייפסקריפט או להזיז אותה הצידה לרגע רק בשביל לתקן את הכפילות? הגישה שלי פה היא יותר פרגמטית. אם לא הצלחתי למצוא את הטיפוס שפותר לי את הבעיה אני שמח לכתוב any בתוך פונקציית עזר בשביל שאוכל להתקדם ולקצר את הקוד. תמיד אפשר יהיה להחליף את זה לבדיקת טיפוסים יותר ספציפית בעתיד כשטייפסקריפט יהיה מספיק חכם.
פיתרון Advent Of Code יום 16 בסקאלה
עבר הרבה זמן מאז שפירסמתי את הפיתרון ליום 15 בסידרת Advent Of Code. אפשר לקרוא לזה משבר האמצע או עומס מסיבות אחרות. בכל מקרה היום נעשה עוד צעד בדרך לסיום כל 25 החידות עד סוף השנה.

האתגר - מבוך המראות
האתגר שלנו היום הוא לדמיין מבוך של מראות וקרן אור שעוברת בין המראות. זה קלט הדוגמה:

.|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|....


הקרן נכנסת מהפינה השמאלית עליונה למבוך ומתקדמת בכיוון ימין. ההתנהגות שלה תלויה בסוג המראה שהיא תפגוש:

1. אם היא פוגשת מראה אלכסונית היא תשנה את הכיוון לפי האלכסון (לדוגמה קרן שהולכת ימינה ופוגשת במראה / תשנה את הכיוון למעלה).

2. אם היא פוגשת מראה ישרה בצד בצד המחודד שלה לא קורה כלום.

3. אם היא פוגשת מראה ישרה בצד השטוח שלה, למשל קרן שהולכת ימינה ופוגשת במראה |, אז הקרן תתפצל ל-2 והקרניים ילכו לשני הצדדים למעלה ולמטה.

האתגר שלנו הוא למצוא בכמה משבצות הקרן עברה.

פיתרון בסקאלה
הטריק החשוב כאן הוא להבין את תנאי העצירה. קרן מפסיקה לעניין אותנו כשהיא יוצאת מהמטריצה כמובן, אבל גם אם היא מגיעה שוב לנקודה שהיא כבר ביקרה בה ובאותו כיוון, כי במצב כזה היא נכנסת למעגל.

לכן הפונקציה המעניינת של הפיתרון היא:

@tailrec
def travel(beams: List[Beam],
matrix: Map[(Int, Int), Char],
visited: Set[Beam] = Set()): Set[Beam] =
val activeBeams = beams.filter { b =>
matrix.contains(b.row, b.column) && !visited.contains(b)
}
if (activeBeams.nonEmpty) {
val nextRound = activeBeams.flatMap { b => step(b, matrix(b.row, b.column)) }
travel(nextRound, matrix, visited ++ activeBeams.toSet)
} else {
visited
}


זו פונקציה רקורסיבית שמקבלת רשימה של קרניים, מטריצה ורשימת מיקומים שביקרנו בהם ומחשבת את רשימת הקרניים הבאה. בשביל זה היא קוראת לפונקציית עזר בשם step שזה המימוש שלה:

  def step(beam: Beam, ch: Char): List[Beam] =
ch match
case '.' => List(beam.continue())
case '|' if vertical(beam.direction) => List(beam.continue())
case '|' if horizontal(beam.direction) => List(beam.up(), beam.down())

case '-' if horizontal(beam.direction) => List(beam.continue())
case '-' if vertical(beam.direction) => List(beam.left(), beam.right())

case '/' if beam.direction == Direction.Right => List(beam.up())
case '/' if beam.direction == Direction.Left => List(beam.down())
case '/' if beam.direction == Direction.Up => List(beam.right())
case '/' if beam.direction == Direction.Down => List(beam.left())

case '\\' if beam.direction == Direction.Right => List(beam.down())
case '\\' if beam.direction == Direction.Left => List(beam.up())
case '\\' if beam.direction == Direction.Up => List(beam.left())
case '\\' if beam.direction == Direction.Down => List(beam.right())


אחרי שכל קרן התקדמה צעד אחד תהיה לנו רשימה של קרניים חדשות, אותן נשלח שוב ל travel וכך הלאה עד שלא יהיו יותר קרניים ברשימה.

סך הכל הפיתרון בסקאלה לחלק הראשון הוא:

import aoc2023day16.Direction
import aoc2023day16.Direction.{Down, Up}

import scala.annotation.tailrec
import scala.io.Source

object aoc2023day16 {
enum Direction {
case Up, Down, Left, Right
}

def vertical(dir: Direction): Boolean = dir == Direction.Up || dir == Direction.Down
def horizontal(dir: Direction): Boolean = dir == Direction.Left || dir == Direction.Right

case class Beam(row: Int, column: Int, direction: Direction) {
def continue(): Beam = {
this.direction match
case Direction.Right => right()
case Direction.Left => left()
case Direction.Up => up()
case Direction.Down => down()
}

def up(): Beam = Beam(this.row - 1, this.column, Direction.Up)
def down(): Beam = Beam(this.row + 1, this.column, Direction.Down)
def left(): Beam = Beam(this.row, this.column - 1, Direction.Left)
1
def right(): Beam = Beam(this.row, this.column + 1, Direction.Right)
}

val demoInput: String =
""".|...\....
||.-.\.....
|.....|-...
|........|.
|..........
|.........\
|..../.\\..
|.-.-/..|..
|.|....-|.\
|..//.|....
|""".stripMargin

def step(beam: Beam, ch: Char): List[Beam] =
ch match
case '.' => List(beam.continue())
case '|' if vertical(beam.direction) => List(beam.continue())
case '|' if horizontal(beam.direction) => List(beam.up(), beam.down())

case '-' if horizontal(beam.direction) => List(beam.continue())
case '-' if vertical(beam.direction) => List(beam.left(), beam.right())

case '/' if beam.direction == Direction.Right => List(beam.up())
case '/' if beam.direction == Direction.Left => List(beam.down())
case '/' if beam.direction == Direction.Up => List(beam.right())
case '/' if beam.direction == Direction.Down => List(beam.left())

case '\\' if beam.direction == Direction.Right => List(beam.down())
case '\\' if beam.direction == Direction.Left => List(beam.up())
case '\\' if beam.direction == Direction.Up => List(beam.left())
case '\\' if beam.direction == Direction.Down => List(beam.right())




@tailrec
def travel(beams: List[Beam],
matrix: Map[(Int, Int), Char],
visited: Set[Beam] = Set()): Set[Beam] =
val activeBeams = beams.filter { b =>
matrix.contains(b.row, b.column) && !visited.contains(b)
}
if (activeBeams.nonEmpty) {
val nextRound = activeBeams.flatMap { b => step(b, matrix(b.row, b.column)) }
travel(nextRound, matrix, visited ++ activeBeams.toSet)
} else {
visited
}

def parseInput(input: Source): Map[(Int, Int), Char] =
input
.getLines()
.zipWithIndex
.collect {
case (line: String, index: Int) =>
line.toList.zipWithIndex.map((ch, column) => (index, column, ch))
}
.flatten
.flatMap { case (row, column, ch) => Map((row, column) -> ch) }
.toMap

def printMatrix(matrix: Map[(Int, Int), Char]): Unit =
val maxRow = matrix.keys.maxBy(_._1)._1
val maxColumn = matrix.keys.maxBy(_._2)._2
0.to(maxRow).foreach { row =>
0.to(maxColumn).foreach { col =>
print(matrix((row, col)))
}
println()
}

def countVisited(start: Beam, matrix: Map[(Int, Int), Char]) =
val visited = travel(List(start), matrix)
visited.map(b => (b.row, b.column)).size

@main
def day16part1(): Unit =
val matrix = parseInput(Source.fromResource("day16.txt"))
println(countVisited(Beam(0, 0, Direction.Right), matrix))
}


בחלק השני ביקשו למצוא נקודת כניסה חלופית לקרן ממנה אפשר יהיה לבקר ביותר משבצות. אני לא מצאתי טריק חכם ופשוט חישבתי בכמה משבצות מבקרים מכל נקודת כניסה אפשרית והחזרתי את זו שהמספר עבורה היה הגדול ביותר. זה הקוד:

  def border(matrix: Map[(Int, Int), Char]): List[Beam] =
val numRows = matrix.keys.maxBy(_._1)._1
val numColumns = matrix.keys.maxBy(_._2)._2
matrix.keys.filter { (row, col) =>
row == 0 || row == numRows || col == 0 || col == numColumns
}.flatMap { (row, col) =>
if (row == 0 && col == 0) {
// top left
List(Beam(0, 0, Direction.Down), Beam(0, 0, Direction.Right))
} else if (row == 0 && col == numColumns) {
// top right
List(Beam(row, col, Direction.Down), Beam(row, col, Direction.Left))
} else if (row == numRows && col == 0) {
// bottom left
List(Beam(row, col, Direction.Up), Beam(row, col, Direction.Right))
} else if (row == numRows && col == numColumns) {
// bottom right
List(Beam(row, col, Direction.Up), Beam(row, col, Direction.Left))
} else if (row == 0) {
// top row
List(Beam(row, col, Direction.Down))
} else if (col == 0) {
// left
List(Beam(row, col, Direction.Right))
} else if (row == numRows) {
// bottom
List(Beam(row, col, Direction.Up))
} else if (col == numColumns) {
// right
List(Beam(row, col, Direction.Left))
} else {
List()
}
}.toList

@main
def day16part2(): Unit =
val matrix = parseInput(Source.fromResource("day16.txt"))
val bestBeam = border(matrix).maxBy { b => countVisited(b, matrix) }
println(countVisited(bestBeam, matrix))


אני חייב להודות שלמרות שהקוד ארוך זה היה אחד התרגילים הקלים של השנה, במיוחד אחרי שכבר היו לי את כל הפונקציות של פיענוח הקלט המטריציוני מתרגילים קודמים.