위키책 kowikibooks https://ko.wikibooks.org/wiki/%EC%9C%84%ED%82%A4%EC%B1%85:%EB%8C%80%EB%AC%B8 MediaWiki 1.44.0-wmf.1 first-letter 미디어 특수 토론 사용자 사용자토론 위키책 위키책토론 파일 파일토론 미디어위키 미디어위키토론 틀토론 도움말 도움말토론 분류 분류토론 TimedText TimedText talk 모듈 모듈토론 동국정운 색인/끝소리 ㅇ 0 7479 45123 45114 2024-10-30T13:04:04Z 2600:1700:C76:9010:22A7:9C6B:AF28:E81F /* ㅑ */ 45123 wikitext text/x-wiki === ㆍ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:008 || ㅈ || ᄌᆞᆼ ˧ 貲訾𤺒訿觜髭𩑽鄑咨諮資䆅姿粢秶澬姕𪗋齊𧞓齍璾齎玆茲孳滋嵫鎡仔孜耔鼒 ᄌᆞᆼ〯 ˧˦ 紫訿𤺒啙呰訾跐㧗姊秭子耔芓仔杍梓榟 ᄌᆞᆼ〮 ˦ 恣積𥡯㧘柴 |- | 5:009 || ㅊ || ᄎᆞᆼ ˧ 雌䳄郪趑次𨀥𨒮❌ ᄎᆞᆼ〯 ˧˦ 此泚佌𠈈𢇌玼皉 ᄎᆞᆼ〮 ˦ 刺𧧒庛❌𢉪次佽㐸絘䯸 |- | 5:010 || ㅉ || ᄍᆞᆼ ˧ 慈㤵磁鶿鷀鴜茲茨餈𩜴𥻓粢䭣薋𩆂䨏𩆃瓷❌薺疵玼骴髊𩨱茈𦶉訾 ᄍᆞᆼ〮 ˦ 字牸孶自漬眥眦骴髊㱴餗 |- | 5:011 || ㅅ || ᄉᆞᆼ ˧ 私思罳䰄偲愢緦颸絲司伺覗𥄶斯撕澌凘𪆁廝㒋虒傂禠㴲磃𩆵菥析徙師獅𤜳篩釃𦌿灑廝襹褷籭簛簁蓰 ᄉᆞᆼ〯 ˧˦ 徙璽死枲葸𤟧諰鰓禗躧蹝屣釃纚縰灑洒矖𥊂𩌦𩎉蓰簁籭漇史駛使 ᄉᆞᆼ〮 ˦ 賜錫瀃澌𣩠杫𣘩㮐四泗柶駟肆𩬶肄笥伺覗司僿思𩢲駛使𩌦屣灑洒曬釃䚕 |- | 5:013 || ㅆ || ᄊᆞᆼ ˧ 詞祠柌辭辤漦 ᄊᆞᆼ〯 ˧˦ 兕𧰽𠒃似仏姒㚶耜梩𦓨杞䎣枱𨐠祀𥘰𧝀汜巳士仕𣐈戺俟竢𥏳𢓪䇃𢈟𢉡逘𣀼𡱢涘騃 ᄊᆞᆼ〮 ˦ 寺嗣飤飼食飴事 |} === ㅣ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:014 || ㅋ || 킹〯 ˧˦ 企跂𠈮 킹〮 ˦ 棄企❌跂蚑 |- | 5:015 || ㄲ || 낑 ˧ 祇軝❌軙忯疧疷伎跂枝蚑𨙸岐歧祁耆鰭愭鬐 |- | 5:015 || ㄷ || 딩 ˧ 知𥎿䝷蜘𧐉鼅胝疷䟡躓𦙁 딩〯 ˧˦ 徵黹𧝉䌤緻絺希鵗 딩〮 ˦ 致致輊輖𦥎䡹質劕躓懫懥驇疐𨇈嚔❌𢷟智知置 |- | 5:016 || ㅌ || 팅 ˧ 摛攡螭彲離𡖟魑离𣉽絺瓻癡𢣕𠈴笞抬𪗪呞 팅〯 ˧˦ 褫搋拸䚦扡扯恥誀祉 팅〮 ˦ 眙 |- | 5:017 || ㄸ || 띵 ˧ 馳䮈池篪竾筂褫簃𠗺謻趨邸踟踶墀𡎰遲遟𨒈赿❌䜄坻汣𡊆沶泜蚳𧐏貾治耛菭持蛇 띵〯 ˧˦ 豸𧋈褫傂廌阤陁陊杝雉鴙鶨薙埃垁滍泜踶峙𠍰跱畤庤𤲵痔偫崻 띵〮 ˦ 地坔埊緻致穉稚䆈䄺䕌謘遲遟治値直植置褫彘 |- | 5:019 || ㄴ || 닝 ˧ 尼㞾怩跜秜旎 닝〯 ˧˦ 柅旎狔 닝〮 ˦ 膩𦡸 |- | 5:020 || ㅂ || 빙 ˧ 卑庳箄裨陴埤鞞錍椑陂波詖羆❌❌罷藣❌❌碑悲非餥裴蜚誹緋扉飛騛 빙〯 ˧˦ 俾卑髀脾鞞箄匕比妣䃾❌秕粃❌朼枇彼佊鄙啚否匪篚榧棐蜚奜蜰 빙〮 ˦ 臂嬖辟畀❌痹比庛芘賁跛詖陂披佊貱波藣祕秘泌柲鉍䪐閟毖鄪費❌粊轡䩛沸髴❌誹非芾茀 |- | 5:022 || ㅍ || 핑 ˧ 紕❌❌悂鈹鉟披㱟㨢被旇翍耚❌丕㔻伾秠怌駓䮆豾狉銔批霏䬠菲騑匪❌馡裶裴妃婓❌ 핑〯 ˧˦ 庀比庇疕仳諀吡批披㱟噽秠❌斐菲誹非悱朏 핑〮 ˦ 譬辟濞淠帔被襬費 |- | 5:024 || ㅃ || 삥 ˧ 毗紕辟𨈚❌❌阰❌魮❌鈚蚍螕琵批比膍肶㮰貔豼❌❌陴埤脾裨崥皮疲罷郫邳岯坯❌魾肥淝腓痱❌厞婓賁 삥〯 ˧˦ 婢埤䠋卑庳埤否痞岯圮䤏被骳罷陫❌ 삥〮 ˦ 鼻比紕枇𢈷庳卑避辟髲被鞁骳備俻犕糒奰贔屝厞陫❌痱腓菲蜚翡䠊剕❌費狒𥜿𥝃𦦻𤲳昲𪎰黂䆊 |- | 5:027 || ㅁ || 밍 ˧ 彌弥𢏏㜷𡝠瀰冞𥹐糜𩞁𩞇𪎭穈𪐎縻𥿫𦄐䌕靡㸏𤓒𦗕劘𪎕𠞧醾𨣿醿蘼蘪攠❌眉嵋湄𣽪𤃰攗楣郿媚麋麛黴微𧗬薇㵟溦霺 밍〯 ˧˦ 弭渳彌瀰𣴱沔敉侎𢘺芉美渼媺靡𦗕尾亹斖娓 밍〮 ˦ 寐媢媚郿篃魅靡未味 |- | 5:028 || ㅈ || 징 ˧ 支枝肢𨈪𨈙跂䧴鳷巵觶𧣨梔氏祇祗衹褆㩼多秖榰禔疧脂祗砥厎泜之芝 징〯 ˧˦ 紙帋坁汦汣❌抵觗㧗抵砥只𠮡軹枳咫𦐖❌疻旨𣅀指𢫾脂恉厎底𦒿止芷茝沚洔阯址趾畤 징〮 ˦ 寘忮伎觶𧣨觗至摯贄質鷙𪁊礩志誌識痣䏯織綕𥿮幟𢂴笫 |- | 5:031 || ㅊ || 칭 ˧ 蚩嗤𢾫媸鴟鵄眵 칭〯 ˧˦ 侈奓鉹𨪐誃哆恀䏧姼𡚼㶴袳移𧛧袲㢋㢁懘齒茝䊼 칭〮 ˦ 熾𧹹幟織旘𢂴志饎糦喜𩛉𩜮𩜂埴𡑌戠 |- | 5:032 || ㅅ || 싱 ˧ 施葹鍦鉇𥍸𥍢絁䌤𦂛䙾𧠜𧠉翅尸㕧鳲𨾋䲩屍蓍詩邿 싱〯 ˧˦ 弛𢐋㢮阤施豕矢笑屎始 싱〮 ˦ 翅𦐊翄翨𦑧施鍦釶啻翅試式弑殺煞𢎍識幟織始失 |- | 5:033 || ㅆ || 씽 ˧ 茬茌匙㮛堤提𦑡禔媞鍉時塒鰣 씽〯 ˧˦ 是諟惿媞𨸝䟗氏視市恃㤄𦧇舐𦧧咶狧 씽〮 ˦ 示謚𧨦豉嗜耆𩝙𨢍𦞯呩視侍寺䦙蒔 |- | 5:034 || ㆆ || ᅙᅵᆼ ˧ 伊洢咿吚黟黝❌ ᅙᅵᆼ〮 ˦ 縊 |- | 5:035 || ㅎ || 힝 ˧ 㕧屎䐖❌❌❌ |- | 5:035 || ㅇ || 잉 ˧ 移拖❌䔟簃❌扅❌栘鉹袲椸箷䈕㮛❌暆貤酏匜詑訑施❌❌蛇夷尼❌痍荑恞𢓡跠峓鐵侇桋䧅姨洟❌胰寅夤彛飴❌❌❌䬮怡眙詒❌貽瓵怠台頤宧圯異 잉〯 ˧˦ 以已苢苡酏祂肔胣匜迆迤施崺❌ 잉〮 ˦ 易㑥敡施袘❌緆衪貤❌移肄肆勩❌廙異異食詒貽 |- | 5:037 || ㄹ || 링 ˧ 離蘺籬杝㰚㒿䍦❌灕漓離攡摛褵縭璃䅻醨离鸝鵹❌❌❌矖纚驪孋穲䕻❌麗罹蠡盠❌劙❌棃❌棃梨犂黧❌犂❌❌犁蔾❌菞釐剺❌氂犛狸剺嫠來倈徠䅘貍狸❌猍梩❌ 링〯 ˧˦ 里理鯉❌俚悝裏李邐麗履 링〮 ˦ 詈茘離离利莅蒞位吏 |- | 5:040 || ㅿ || ᅀᅵᆼ ˧ 兒唲而胹臑腝❌❌洏聏陑栭鮞髵耏耐鴯怠❌‗ 濡 ᅀᅵᆼ〯 ˧˦ 爾爾爾邇邇邇耳耳珥駬䋙餌柅鑈檷 ᅀᅵᆼ〮 ˦ 二貳㒃樲餌㢽❌珥鉺衈咡刵毦髶聏 |} === ㆎ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:041 || ㄷ || ᄃᆡᆼ〮 ˦ 戴載襶 |- | 5:041 || ㅌ || ᄐᆡᆼ ˧ 胎孡台能駘鮐邰斄釐漦 ᄐᆡᆼ〮 ˦ 貸態詒紿怠䈚箈殆待 |- | 5:041 || ㄸ || ᄄᆡᆼ ˧ 臺𡌬薹籉儓擡鮐駘跆苔炱炲䈚箈 ᄄᆡᆼ〯 ˧˦ 待𥩳逮迨隶𨽿駘紿詒殆怠䈚箈靆 ᄄᆡᆼ〮 ˦ 代岱袋黛逮曃靆棣埭𥓏𡍖瑇❌毒玳 |- | 5:043 || ㅂ || ᄇᆡᆼ ˧ 杯盃𦈶𦈧𠤯桮 ᄇᆡᆼ〮 ˦ 背輩軰 |- | 5:043 || ㅍ || ᄑᆡᆼ ˧ 胚妚衃阫坏培醅❌ ᄑᆡᆼ〯 ˧˦ 朏 ᄑᆡᆼ〮 ˦ 配妃朏 |- | 5:043 || ㅃ || ᄈᆡᆼ ˧ 裴徘俳培陪倍阫 ᄈᆡᆼ〯 ˧˦ 琲㻗痱❌倍 ᄈᆡᆼ〮 ˦ 佩佩背偝負倍㔨北邶鄁倍焙❌孛茀勃誖悖哱怫㫲琲㻗拔萯 |- | 5:044 || ㅁ || ᄆᆡᆼ ˧ 枚玫梅楳某槑❌鋂脢脄❌膴酶䊈莓每䍙禖媒煤塺 ᄆᆡᆼ〯 ˧˦ 浼每痗脢脄 ᄆᆡᆼ〮 ˦ 妹昧㫚眛沬韎❌每痗脢脄沕琩❌冒媒 |- | 5:046 || ㅈ || ᄌᆡᆼ ˧ 哉裁栽𦳦載災灾菑甾𤆄 ᄌᆡᆼ〯 ˧˦ 宰䏁縡載 ᄌᆡᆼ〮 ˦ 再載縡 |- | 5:046 || ㅊ || ᄎᆡᆼ ˧ 猜偲 ᄎᆡᆼ〯 ˧˦ 采採寀彩棌綵䰂茝 ᄎᆡᆼ〮 ˦ 菜寀采縩 |- | 5:046 || ㅉ || ᄍᆡᆼ ˧ 裁才材財鼒纔 ᄍᆡᆼ〯 ˧˦ 在 ᄍᆡᆼ〮 ˦ 在裁栽載酨 |- | 5:047 || ㅅ || ᄉᆡᆼ ˧ 鰓䚡䰄思罳❌顋毸 ᄉᆡᆼ〮 ˦ 塞僿簺賽 |- | 5:047 || ㆆ || ᅙᆡᆼ ˧ 哀埃㶼欸唉 ᅙᆡᆼ〯 ˧˦ 欸唉款靉靄藹娭毐 ᅙᆡᆼ〮 ˦ 愛靉曖僾薆藹靄欸 |- | 5:048 || ㅎ || ᄒᆡᆼ ˧ 咍 ᄒᆡᆼ〯 ˧˦ 海❌醢❌ |- | 5:048 || ㆅ || ᅘᆡᆼ ˧ 孩㜾❌頦 ᅘᆡᆼ〯 ˧˦ 亥侅劾 ᅘᆡᆼ〮 ˦ 瀣劾 |- | 5:048 || ㄹ || ᄅᆡᆼ ˧ 來逨❌倈萊徠䅘❌騋淶崍 ᄅᆡᆼ〮 ˦ 徠倈來睞賚萊 |} === ㅢ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:049 || ㄱ || 긩 ˧ 羈䩭鞿羇踦奇倚踦奇畸觭掎剞飢饑❌肌❌姬箕❌❌箕諆❌錤棋檱踑其基朞期祺居機鐖璣禨嘰饑鞿磯譏耭蟣幾刏 긩〯 ˧˦ 己紀几機㲹麂掎踦剞❌庋攱庪㨳蟣❌穖幾 긩〮 ˦ 寄徛❌冀兾驥❌懻❌穊❌穖覬幾洎記其忌已㤅近旣漑曁 |- | 5:051 || ㅋ || 킝 ˧ 敧崎觭踦䗁徛碕榿欺倛僛❌娸❌❌❌ 킝〯 ˧˦ 綺觭起杞梩❌㞯玘芑豈䔇 킝〮 ˦ 器❌器亟氣❌乞❌ |- | 5:052 || ㄲ || 끵 ˧ 奇騎錡琦碕埼隑其萁稘期琪淇祺麒騏鶀❌旗棊碁櫀蜝綦綨❌❌𤪌璂蘄祈❌蘄肵旂頎畿圻幾❌刏❌機碕 끵〯 ˧˦ 技伎妓䗁錡跽❌ 끵〮 ˦ 芰茤騎䗁妓技❌洎垍曁❌墍蔇忌鵋誋惎㥍諅綦禨璣幾曁刏 |- | 5:054 || ㆁ || ᅌᅴᆼ ˧ 宜義儀❌檥轙議鸃䴊㕒嶬涯崖疑儗嶷觺沂凒皚溰 ᅌᅴᆼ〯 ˧˦ 蟻蟻蛾❌❌轙艤檥礒硪齮錡擬譺懝儗疑薿❌孴❌矣顗 ᅌᅴᆼ〮 ˦ 義誼❌竩議儗❌毅藙 |- | 5:055 || ㅈ || 즹 ˧ 菑載𤰭䅔𦿨淄甾緇純䊷輜椔錙鶅𨿴甾 즹〯 ˧˦ 滓胏𦞤𦙰笫緇 즹〮 ˦ 胾椔緇菑倳事輜剚𨧫 |- | 5:056 || ㅊ || 칑 ˧ 差嵯嵳䡨𨍃 칑〮 ˦ 廁厠 |- | 5:056 || ㆆ || ᅙᅴᆼ ˧ 漪猗䝝兮椅欹旖禕醫毉噫意億懿嘻譆❌衣依譩 ᅙᅴᆼ〯 ˧˦ 倚奇椅檹輢猗旖㫊䭲譩醫醷臆旖㕈庡依偯 ᅙᅴᆼ〮 ˦ 意薏倚猗輢懿饐❌饖撎衣䵝 |- | 5:058 || ㅎ || 힁 ˧ 犧曦㬢爔羲戲嚱❌❌❌❌檥巇嶬桸獻僖嬉嘻禧釐譆熹暿熺熙娭㜯稀俙晞睎豨狶欷唏鵗希 힁〯 ˧˦ 喜憘憙嬉豨唏俙霼 힁〮 ˦ 戲齂哂嚊屭豷燹咥憙喜憘嬉欷唏愾餼䊠旣熂霼墍❌❌摡❌黖 |} === ㅚ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:060 || ㄱ || 굉 ˧ 傀❌❌❌❌瑰瓌璝 굉〮 ˦ 儈會襘檜澮膾鱠獪䯤鬠❌旝鄶劊會廥憒慖幗蔮刏 |- | 5:061 || ㅋ || 쾽 ˧ 恢❌詼盔魁❌悝 쾽〮 ˦ ❌塊蕢蒯❌堁 |- | 5:061 || ㆁ || ᅌᅬᆼ ˧ 嵬峞隗阢桅磑 ᅌᅬᆼ〯 ˧˦ 隗嵬峞䫥頠 ᅌᅬᆼ〮 ˦ 外磑 |- | 5:061 || ㄷ || 됭 ˧ 鎚追❌頧搥槌磓堆❌塠㕍❌❌敦 됭〮 ˦ 祋杸對轛❌碓敦 |- | 5:062 || ㅌ || 툉 ˧ 推蓷焞 툉〯 ˧˦ 腿 툉〮 ˦ 娧脫稅駾蛻兌退𨑧𢓇 |- | 5:063 || ㄸ || 뙹 ˧ 隤頹墤穨尵僓魋 뙹〯 ˧˦ 鐓憝隊 뙹〮 ˦ 兌𩊭銳𠜑奪隊䨴𩅥𩄮薱憝譈懟憞譵鐓駾 |- | 5:063 || ㄴ || 뇡 ˧ 挼捼 뇡〯 ˧˦ 餧餒鯘鮾脮腇腇 뇡〮 ˦ 內 |- | 5:064 || ㅈ || 죙〮 ˦ 最蕞棷粹晬綷❌祽 |- | 5:064 || ㅊ || 쵱 ˧ 崔催縗衰䙑 쵱〯 ˧˦ 漼璀皠洒綷萃 쵱〮 ˦ 襊㝮竄倅萃淬焠𠗚啐啛 |- | 5:065 || ㅉ || 쬥 ˧ 摧漼凗崔㠑磪𡹐 쬥〯 ˧˦ 辠𡽕 쬥〮 ˦ 蕞 |- | 5:065 || ㅅ || 쇵 ˧ 𣯧𦸏挼䪎鞖 쇵〮 ˦ 碎粹誶 |- | 5:065 || ㆆ || ᅙᅬᆼ ˧ 隈❌椳煨䋿偎畏 ᅙᅬᆼ〯 ˧˦ 猥❌椳萎 ᅙᅬᆼ〮 ˦ 薈懀濊 |- | 5:065 || ㅎ || 횡 ˧ 灰❌䝇豗拻 횡〯 ˧˦ 賄悔 횡〮 ˦ ❌噦鐬翽誨晦悔❌靧頮 |- | 5:066 || ㆅ || ᅘᅬᆼ ˧ 回廻茴洄徊瑰槐瘣 ᅘᅬᆼ〯 ˧˦ 瘣壞廆匯滙回 ᅘᅬᆼ〮 ˦ 會禬繪䙡璯潰繪繪䜋聵瞶闠回匯䔇 |- | 5:067 || ㄹ || 룅 ˧ 雷䨓❌罍❌鑘𨯔❌礧轠瓃❌ 룅〯 ˧˦ 磊磥礌礧❌㠥礨壘櫑儡❌❌❌㒦❌蕾 룅〮 ˦ 酹類耒礧壘礌雷㵢攂 |} === ㅐ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:068 || ㄱ || 갱 ˧ 佳街皆偕湝階堦楷喈❌荄痎䕸稭祴該垓畡陔閡絯胲侅晐峐剴祴裓 갱〯 ˧˦ 解改胲❌陔閡 갱〮 ˦ 蓋蓋匃匄丐懈解繲廨解戒誡悈介界堺❌魪玠琾㠹价尬芥疥蚧❌䯰❌屆漑摡槪扢槪㧉剴 |- | 5:070 || ㅋ || 캥 ˧ 揩❌❌❌開 캥〯 ˧˦ 鍇楷愷凱豈鎧塏闓 캥〮 ˦ 磕礚愒❌渴嘅❌揩慨鎧闓欬咳愾 |- | 5:070 || ㆁ || ᅌᅢᆼ ˧ 厓崖涯漄睚倪皚❌凒敱 ᅌᅢᆼ〯 ˧˦ 騃 ᅌᅢᆼ〮 ˦ 艾❌㣻礙硋㝵儗閡 |- | 5:071 || ㄷ || 댕〮 ˦ 帶㿃蔕蹛遞 |- | 5:071 || ㅌ || 탱〮 ˦ 泰太太大忕㥭汰汏蠆𧀱蔕慸 |- | 5:071 || ㄸ || 땡〯 ˧˦ 廌豸豸 땡〮 ˦ 大軑釱汏 |- | 5:072 || ㄴ || 냉 ˧ 能 냉〯 ˧˦ 嬭㛋㚷乃迺鼐 냉〮 ˦ 柰耐𦓎能𣉘奈鼐 |- | 5:072 || ㅂ || 뱅〯 ˧˦ 擺❌捭 뱅〮 ˦ 貝狽伂沛拜扒敗 |- | 5:072 || ㅍ || 팽〮 ˦ 霈沛肺浿派湃㵒 |- | 5:073 || ㅃ || 뺑 ˧ 牌❌❌蠯螷❌排俳 뺑〯 ˧˦ 罷 뺑〮 ˦ 旆沛茷❌柭軷粺❌稗❌❌韛❌❌排糒敗唄 |- | 5:073 || ㅁ || 맹 ˧ 埋貍霾䨪 맹〯 ˧˦ 買 맹〮 ˦ 眛昧沬賣韎霾邁❌勱佅 |- | 5:074 || ㅈ || 쟁 ˧ 齋齊❌ 쟁〯 ˧˦ 跐 쟁〮 ˦ 債責瘵祭 |- | 5:074 || ㅊ || 챙 ˧ 釵靫扠搋差䡨 챙〮 ˦ 蔡❌縩瘥差衩涇 |- | 5:075 || ㅉ || 쨍 ˧ 柴祡豺犲儕 쨍〮 ˦ 眦❌疵眥砦柴 |- | 5:075 || ㅅ || 생 ˧ 簁篩 생〯 ˧˦ 灑洒躧蹝纚徙 생〮 ˦ 曬灑洒鎩铩殺閷煞 |- | 5:076 || ㆆ || ᅙᅢᆼ ˧ 娃哇❌挨唲 ᅙᅢᆼ〯 ˧˦ 矮㾨躷 ᅙᅢᆼ〮 ˦ 藹靄䨠馤壒❌濭隘阨搤噫❌呃嗄餲喝 |- | 5:076 || ㆅ || ᅘᅢᆼ ˧ 諧湝骸膎鞵鞋鮭 ᅘᅢᆼ〯 ˧˦ 蟹蟹獬❌鮭嶰澥解駭絯豥駴夥 ᅘᅢᆼ〮 ˦ 害妎邂解械❌薤❌瀣齘 |- | 5:077 || ㄹ || 랭〮 ˦ 賴瀨籟藾癩襰厲糲蠣賚 |} === ㅙ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:077 || ㄱ || 괭 ˧ 乖❌媧緺騧❌蝸 괭〯 ˧˦ 掛 괭〮 ˦ 卦掛挂絓罣詿怪怪壞夬澮獪㹟狤 |- | 5:078 || ㅋ || 쾡 ˧ ❌華䦱 쾡〮 ˦ 蒯❌蕢塊喟嘳㕟快噲 |- | 5:078 || ㆁ || ᅌᅫᆼ〮 ˦ 聵❌❌ |- | 5:078 || ㅊ || 쵕〮 ˦ 嘬 |- | 5:078 || ㆆ || ᅙᅫᆼ ˧ 蛙鼃洼 |- | 5:078 || ㆅ || ᅘᅫᆼ ˧ 懷櫰槐褢❌❌淮 ᅘᅫᆼ〮 ˦ 壞畵罫繣澅絓話❌躗吳 |} === ㅟ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:079 || ㄱ || 귕 ˧ 嬀潙佹龜騩歸 귕〯 ˧˦ 軌軌軌匭氿漸宄❌晷簋詭垝陒佹❌祪䤥庪庋攱傀鬼 귕〮 ˦ 媿愧❌聭騩攰庪貴瞶劌劂❌撅橛蹶❌❌鱖鱥 |- | 5:080 || ㅋ || 큉 ˧ 虧𧇾𩏣蘬巋 큉〯 ˧˦ 巋蘬 큉〮 ˦ 喟嘳㕟䙡巋缺 |- | 5:081 || ㄲ || 뀡 ˧ 逵㙺夔犪騤戣鍨頯頄馗 뀡〯 ˧˦ 跪 뀡〮 ˦ 匱鐀櫃饋歸蕢䕚簣籄㙺 |- | 5:081 || ㆁ || ᅌᅱᆼ ˧ 危峗峞帷爲韋幃褘湋違闈圍巍嵬犩❌ ᅌᅱᆼ〯 ˧˦ 頠姽峗瓦韙偉瑋煒颹暐韡❌葦 ᅌᅱᆼ〮 ˦ 僞胃❌謂渭蝟猬❌㥜媦緯圍彙❌❌魏犩❌衛熭❌❌轊䡺❌❌彘㻰 |- | 5:082 || ㄷ || 뒹〮 ˦ 轛 |- | 5:082 || ㆆ || ᅙᅱᆼ ˧ 逶倭䴧蜲痿萎委威葳蝛 ᅙᅱᆼ〯 ˧˦ 委骩磈嵬 ᅙᅱᆼ〮 ˦ 餧❌委骩尉慰蔚霨罻畏威 |- | 5:083 || ㅎ || 휭 ˧ 麾戲摩撝暉煇煒輝揮楎椲翬❌褘徽幑㫎 휭〯 ˧˦ 毁毁燬䅏❌毇❌烜䃣蟲虺蘬❌❌燬卉 휭〮 ˦ 毁諱卉 |- | 5:084 || ㅇ || 윙〯 ˧˦ 蔿蘤䦱❌❌ 윙〮 ˦ 位爲 |} === ㆌ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:084 || ㄱ || ᄀᆔᆼ ˧ 規槼摫瞡❌雉鳺 ᄀᆔᆼ〯 ˧˦ 癸湀 ᄀᆔᆼ〮 ˦ 季 |- | 5:084 || ㅋ || ᄏᆔᆼ ˧ 闚窺 ᄏᆔᆼ〯 ˧˦ 跬頃蹞窺❌頍頯 |- | 5:085 || ㄲ || ᄁᆔᆼ ˧ 葵楑鄈 ᄁᆔᆼ〯 ˧˦ 揆 ᄁᆔᆼ〮 ˦ 悸 |- | 5:085 || ㄷ || ᄃᆔᆼ ˧ 腄追䨨 |- | 5:085 || ㄸ || ᄄᆔᆼ ˧ 椎桘鎚錘槌甀魋椎鬌 ᄄᆔᆼ〮 ˦ 縋䋘槌膇㾽硾倕磓墜錘腄甀墜隊隧❌懟譵 |- | 5:086 || ㄴ || ᄂᆔᆼ〮 ˦ 諉 |- | 5:086 || ㅈ || ᄌᆔᆼ ˧ 劑檇㰎崔墔隹崒❌厜隹萑鵻騅錐 ᄌᆔᆼ〯 ˧˦ 觜❌嘴❌嶉捶棰錘箠菙 ᄌᆔᆼ〮 ˦ 醉檇雋惴諈錘 |- | 5:087 || ㅊ || ᄎᆔᆼ ˧ 吹龡炊推蓷衰 ᄎᆔᆼ〯 ˧˦ 趡揣 ᄎᆔᆼ〮 ˦ 翠臎吹䶴龡出 |- | 5:087 || ㅉ || ᄍᆔᆼ〮 ˦ 萃瘁悴顇踤 |- | 5:087 || ㅅ || ᄉᆔᆼ ˧ 綏浽荽荾䒘雖睢濉衰榱 ᄉᆔᆼ〯 ˧˦ 髓髓❌䯝䯝❌膸瀡靃❌巂嶲水準 ᄉᆔᆼ〮 ˦ 邃粹睟誶祟帥帨率 |- | 5:087 || ㅆ || ᄊᆔᆼ ˧ 隨隋遺垂陲倕❌腄誰唯譙脽 ᄊᆔᆼ〯 ˧˦ 菙❌ ᄊᆔᆼ〮 ˦ 遂燧❌鐩澻㴚襚❌璲繸❌❌隧❌隊旞❌❌彗篲穗❌穟瑞睡倕 |- | 5:089 || ㆆ || ᅙᆔᆼ〮 ˦ 恚烓 |- | 5:089 || ㅎ || ᄒᆔᆼ ˧ 墮❌❌隳隋綏觽鐫鐫睢倠 ᄒᆔᆼ〮 ˦ 睢隋綏墮挼侐 |- | 5:090 || ㅇ || ᄋᆔᆼ ˧ 惟維唯濰遺壝蠵❌❌觿 ᄋᆔᆼ〯 ˧˦ 洧鮪痏䔺❌芛芟唯踓趡壝 ᄋᆔᆼ〮 ˦ 遺❌壝瞶蜼 |- | 5:090 || ㄹ || ᄅᆔᆼ ˧ 羸纍縲累欙樏㹎❌虆藟❌蘲蔂絫 ᄅᆔᆼ〯 ˧˦ 壘藟虆蘽櫐讄❌❌❌礧❌㠥❌礨礨❌蠝❌累樏誄蜼猚 ᄅᆔᆼ〮 ˦ 類禷蘱率淚累絫 |- | 5:092 || ㅿ || ᅀᆔᆼ ˧ 甤蕤苼痿緌綏䅑䅗桵㮃 ᅀᆔᆼ〯 ˧˦ 繠橤蘂❌蕊 |} === ㅖ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:008 || ㄱ || 곙 ˧ 雞稽𮃛笄䈕枅𣓖卟乩 곙〮 ˦ 罽𦇧𣯅𦆡𣽄猘狾計繼㡭繫毄係❌髻結𨜒薊 |- | 6:008 || ㅋ || 콍 ˧ 谿溪磎嵠鸂䳶 콍〯 ˧˦ 啓棨𢧊綮启稽 콍〮 ˦ 愒憇𢠾偈❌揭䔾甈契栔鍥㓶挈 |- | 6:009 || ㄲ || 꼥〮 ˦ 偈碣嵑 |- | 6:009 || ㆁ || ᅌᅨᆼ〯 ˧˦ 掜 ᅌᅨᆼ〮 ˦ ❌甈乂刈㣻艾 |- | 6:010 || ㄷ || 뎽 ˧ 氐互低仾詆呧柢羝牴隄䧑堤鞮䩚磾 뎽〯 ˧˦ 邸䣌柢詆呧阺坻弤抵掋牴觝軝疧疷底㡳氐提堤底 뎽〮 ˦ 帝諦諟𧫚締蔕䗖柢泜氐疐嚔㗣 |- | 6:011 || ㅌ || 톙 ˧ 梯鷈鷉鵜𩀗 톙〯 ˧˦ 體軆躰涕緹紙醍 톙〮 ˦ 替暜朁涕洟鬀鬄剃殢揥楴𧝐褅裼薙雉𡲕傺袲 |- | 6:012 || ㄸ || 똉 ˧ 題提媞偍姼禔褆緹鞮睼瑅騠踶鶗嗁啼諦㖒㖷渧蹏蹄締綈䬾稊𦯔稺䄺𧀾銻𧋘鵜鴺罤䨑荑梯鮧鯷桋鴺㡗折 똉〯 ˧˦ 弟悌娣遞递𮞏 똉〮 ˦ 第弟悌娣睇眱珶遞迭递𮞏遰褅締杕軑釱踶提題鬄髢錫鶗鷤逮棣滯㿃䐭彘 |- | 6:014 || ㄴ || 녱 ˧ 泥埿臡 녱〯 ˧˦ 禰祢檷鑈鈮濔瀰薾爾泥 녱〮 ˦ 泥 |- | 6:015 || ㅂ || 볭 ˧ 篦豍❌㡙鎞❌狴❌ 볭〮 ˦ 閉箄嬖蔽草弊弊鷩鄨廢癈祓苃茀 |- | 6:015 || ㅍ || 폥 ˧ ❌批錍鈚漂 폥〮 ˦ 媲睤❌䁹辟壀埤僻濞淠潎❌肺杮 |- | 6:016 || ㅃ || 뼹 ˧ 鼙鞞𧯿椑㼰膍 뼹〯 ˧˦ 陛陛梐狴陛髀䯗䏶❌脾肶 뼹〮 ˦ 弊獘斃弊幣❌❌薜蘗❌吠犻❌茷 |- | 6:017 || ㅁ || 몡 ˧ 迷麛麑❌❌ 몡〯 ˧˦ 米眯䋛❌洣 몡〮 ˦ 袂 |- | 6:017 || ㅈ || 졩 ˧ 齎䝴韲齏❌虀懠❌擠躋隮齊❌❌ 졩〯 ˧˦ 濟❌㧗 졩〮 ˦ 霽擠濟❌祭際穄制製䱥鰶㫼晣晢淛浙折 |- | 6:018 || ㅊ || 촁 ˧ 妻萋霋凄淒悽緀 촁〯 ˧˦ 泚玼萋緀 촁〮 ˦ 切砌墄❌妻掣❌摯觢挈懘滯❌ |- | 6:018 || ㅉ || 쪵 ˧ 齊臍蠐 쪵〯 ˧˦ 薺齊癠鮆鱭 쪵〮 ˦ 嚌懠穧❌劑齊薺癠齌眥眦 |- | 6:019 || ㅅ || 솅 ˧ 西栖棲犀屖遟凘澌嘶撕 솅〯 ˧˦ 洗灑 솅〮 ˦ 細壻壻棲世貰勢埶殺閷 |- | 6:020 || ㅆ || 쏑 ˧ 栘 쏑〮 ˦ 誓逝遰遞筮❌噬澨澨忕 |- | 6:020 || ㆆ || ᅙᅨᆼ ˧ 鷖繄䃜瑿㙠黳 ᅙᅨᆼ〮 ˦ 瘞❌餲医瞖繄翳蘙嫕瘱曀㙪㙠殪縊 |- | 6:020 || ㅎ || 혱 ˧ 醯❌䤈橀 |- | 6:021 || ㆅ || ᅘᅨᆼ ˧ 兮❌奚㜎傒蹊❌騱貕豀謑鼷嵇 ᅘᅨᆼ〯 ˧˦ 徯諐蹊❌傒謑謑奚 ᅘᅨᆼ〮 ˦ 系繫係結禊妎盻 |- | 6:021 || ㅇ || 옝 ˧ 倪齯鯢蜺䘽輗❌棿麑猊猊霓兒 옝〮 ˦ 曳洩❌袣❌泄呭詍㖂䛖枻栧抴跇裔❌㵝㵩㳿㵩勩詣栺睨倪堄帠羿❌寱藝蓺❌槸褹袂囈 |- | 6:022 || ㄹ || 롕 ˧ 黎❌❌犁黧藜蔾瓈邌梨驪鸝蠡 롕〯 ˧˦ 禮澧醴鱧蠡䗍劙盠欐 롕〮 ˦ 麗䕻儷儷❌欐攦戾唳淚綟棙悷❌蜧茘❌蛠❌❌隷隷劙蠫❌㓯盭沴離例列栵迾裂厲厲礪❌❌糲爄勵蠣❌癘❌㾐❌ |} === ㆋ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:024 || ㄱ || ᄀᆒᆼ ˧ 圭窐❌❌❌閨袿邽蠲 ᄀᆒᆼ〮 ˦ 桂筀䳏 |- | 6:024 || ㅋ || ᄏᆒᆼ ˧ 睽聧奎暌刲㨒 |- | 6:025 || ㄷ || ᄃᆒᆼ〮 ˦ 綴餟醊腏畷錣 |- | 6:025 || ㅈ || ᄌᆒᆼ〮 ˦ 蕝蕞贅 |- | 6:025 || ㅊ || ᄎᆒᆼ〮 ˦ 脃膬毳橇竁喙 |- | 6:025 || ㅅ || ᄉᆒᆼ〮 ˦ 歲繐❌稅帨帥蛻涗裞䬽鐫說 |- | 6:025 || ㅆ || ᄊᆒᆼ〮 ˦ 篲 |- | 6:025 || ㆆ || ᅙᆒᆼ ˧ 烓❌ ᅙᆒᆼ〮 ˦ 穢薉濊獩 |- | 6:026 || ㅎ || ᄒᆒᆼ〮 ˦ 嘒嚖❌暳噦喙𣨶𤸁𠿔顪噦翽 |- | 6:026 || ㆅ || ᅘᆒᆼ ˧ 攜㩗携觿鑴蠵酅❌巂畦 ᅘᆒᆼ〮 ˦ 慧轊惠蕙譓憓 |- | 6:026 || ㅇ || ᄋᆒᆼ〮 ˦ 叡睿銳梲 |- | 6:027 || ㅿ || ᅀᆒᆼ〮 ˦ 汭內枘芮蜹 |} === ㅗ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:027 || ㄱ || 공 ˧ 孤❌觚❌柧軱呱❌箛䉉苽菰罛❌姑酤沽蛄鴣䧸辜❌❌橭❌盬夃 공〯 ˧˦ 古詁故估牯沽酤罟苦楛鼓❌瞽鼓股❌骰羖❌蠱監賈夃 공〮 ˦ 顧雇固凅錮鯝稒㧽痼故酤沽詁蠱 |- | 6:028 || ㅋ || 콩 ˧ 枯刳恗 콩〯 ˧˦ 苦䇢 콩〮 ˦ 庫袴絝胯跨❌ |- | 6:029 || ㆁ || ᅌᅩᆼ ˧ 吾浯梧鼯❌❌郚吳㻍珸鋘鋙 ᅌᅩᆼ〯 ˧˦ 五伍午迕仵旿 ᅌᅩᆼ〮 ˦ 誤悞悟晤旿晤捂梧忤牾午蘁逜寤❌遻❌迕俉愕 |- | 6:029 || ㄷ || 동 ˧ 都闍堵 동〯 ˧˦ 覩堵❌陼暏賭❌楮肚士 동〮 ˦ 妒妬奼㓃宅詫秅秺蠧螙❌斁❌睪 |- | 6:030 || ㅌ || 통 ˧ 稌 통〯 ˧˦ 土吐稌 통〮 ˦ 兎鵵菟吐 |- | 6:031 || ㄸ || 똥 ˧ 徒途墿峹𡷣荼𣘻余涂捈駼鵌鷋䅷䔑塗屠瘏菟𧈋檡兎圖鍍 똥〯 ˧˦ 杜荰土赭𢾅剫 똥〮 ˦ 度渡𣳥鍍塗 |- | 6:031 || ㄴ || 농 ˧ 奴㚢仅孥帑駑笯砮 농〯 ˧˦ 怒弩砮努 농〮 ˦ 笯怒㣽𢘂 |- | 6:032 || ㅂ || 봉 ˧ 逋餔哺誧晡 봉〯 ˧˦ 補䋠❌圃甫❌譜諩 봉〮 ˦ 布尃抪圃 |- | 6:032 || ㅍ || 퐁 ˧ 鋪痡鯆❌❌抪 퐁〯 ˧˦ 普浦誧溥 퐁〮 ˦ 怖鋪誧 |- | 6:033 || ㅃ || 뽕 ˧ 蒲莆蒱酺匍扶䒀瓿 뽕〯 ˧˦ 簿部蔀 뽕〮 ˦ 步❌荹捕哺餔䊇❌酺脯鞴 |- | 6:033 || ㅁ || 몽 ˧ 模橅❌❌謨謩嫫❌❌膜摹摸撫墓母 몽〯 ˧˦ 姥莽茻❌姆 몽〮 ˦ 暮慕募墓謨慔 |- | 6:034 || ㅈ || 종 ˧ 租蒩菹𦼬𧂚𧀽𦵔𦯓苴 종〯 ˧˦ 祖珇駔組阻岨俎柤𤕲齟詛 종〮 ˦ 作詛𥛜謯作 |- | 6:035 || ㅊ || 총 ˧ 麤麆粗觕初 총〯 ˧˦ 楚礎濋憷❌ 총〮 ˦ 措厝錯醋楚 |- | 6:035 || ㅉ || 쫑 ˧ 徂且鉏鋤助耡 쫑〯 ˧˦ 粗麆𧇿 쫑〮 ˦ 祚胙飵阼葄助耡鉏莇 |- | 6:036 || ㅅ || 송 ˧ 蘇穌㢝酥𦣑𨢭蔬疏疎𤕠梳𣐌𣓜疋綀釃斯 송〯 ˧˦ 所𢨷䝪糈 송〮 ˦ 素愫榡傃嗉訴𧪜愬㴑遡溯泝塑塐疏𥿇 |- | 6:037 || ㆆ || ᅙᅩᆼ ˧ 烏於杇圬釫❌汙汚於嗚惡 ᅙᅩᆼ〯 ˧˦ 塢䃖埡瑦鄔 ᅙᅩᆼ〮 ˦ 汙洿盓❌惡䛩䜑堊嗚 |- | 6:037 || ㅎ || 홍 ˧ 呼虖謼嘑滹❌淲❌❌泘膴幠芋 홍〯 ˧˦ 虎琥滸滹許 홍〮 ˦ 謼嘑呼戽 |- | 6:038 || ㆅ || ᅘᅩᆼ ˧ 胡❌𠴱箶❌湖瑚餬❌䭌醐❌糊粘❌❌❌䉿❌鶘乎虖壷弧狐❌瓠葫 ᅘᅩᆼ〯 ˧˦ 戶昈雇❌扈❌怙❌岵祜酤楛鄠❌下芐芦嫭嫮橭 ᅘᅩᆼ〮 ˦ 護濩頀䪝穫互枑冱瓠嫮嫭涸 |- | 6:039 || ㄹ || 롱 ˧ 盧蘆籚廬鑢爐櫨鱸壚艫轤瓐黸獹瀘矑纑顱髗❌❌玈❌❌ 롱〯 ˧˦ 魯櫓櫓㯭艣虜擄鹵滷塷瀂❌ 롱〮 ˦ 路璐露簬❌鷺❌鸕鴼潞輅賂 |} === ㅏ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:041 || ㄱ || 강 ˧ 歌謌❌戨❌牁哥柯鴚舸渮荷渮嘉佳加笳茄鴐駕珈耞跏迦痂枷葭麚豭猳家 강〯 ˧˦ 哿笴舸菏荷❌賈檟榎夏假徦嘏椵斝斝 강〮 ˦ 箇個介駕架榢枷稼嫁幏假叚下斝斝價賈 |- | 6:042 || ㅋ || 캉 ˧ 珂砢軻呿 캉〯 ˧˦ 可岢軻坷 캉〮 ˦ 軻坷髂䯊❌ |- | 6:043 || ㆁ || ᅌᅡᆼ ˧ 莪峨峨我娥俄哦睋蛾䖸鵝❌䳘鵞牙芽枒㧎衙涯厓 ᅌᅡᆼ〯 ˧˦ 我騀硪峨雅鴉庌牙疋 ᅌᅡᆼ〮 ˦ 餓訝迓御輅衙庌砑牙 |- | 6:044 || ㄷ || 당 ˧ 多多奓 당〯 ˧˦ 嚲癉哆打 당〮 ˦ 癉憚咤喥䖳㓃宅❌奼哆奓 |- | 6:044 || ㅌ || 탕 ˧ 佗他拕拖扡蛇詑訑 탕〯 ˧˦ 姹 탕〮 ˦ 詫❌咤侘差 |- | 6:045 || ㄸ || 땅 ˧ 駝駞它沱沲池陀岮陁阤跎❌佗紽酡鮀迤池鼉鱓鱣驒馱秅茶𣘻涂塗 땅〯 ˧˦ 拕拖扡佗柁舵䑨杕袉沱❌❌沲阤陀陊爹 땅〮 ˦ 馱他大 |- | 6:046 || ㄴ || 낭 ˧ 那難單儺拏挐笯 낭〯 ˧˦ 娜那袲橠難❌旎儺 낭〮 ˦ 柰那哪㖠 |- | 6:047 || ㅂ || 방 ˧ 波碆番僠嶓巴笆芭豝❌羓鈀 방〯 ˧˦ 跛❌簸播把 방〮 ˦ 播譒磻簸霸伯灞壩䃻靶弝杷欛 |- | 6:048 || ㅍ || 팡 ˧ 頗❌坡岥❌陂❌玻葩苩舥芭 팡〯 ˧˦ 頗叵 팡〮 ˦ 破帊帕袙怕❌❌ |- | 6:048 || ㅃ || 빵 ˧ 婆鄱番皤❌番杷爬把琶 빵〮 ˦ 縛杷䆉❌罷 |- | 6:049 || ㅁ || 망 ˧ 摩擵磨❌麼❌䯢魔劘攠麻菻蟆䗫蟇 망〯 ˧˦ 麼䯢馬 망〮 ˦ 磨禡貊伯榪罵䣕䣖鬕 |- | 6:049 || ㅈ || 장 ˧ 樝❌𣕈柤齟 장〯 ˧˦ 左𡯛鮓䱹𩽫苴苲 장〮 ˦ 左佐𡯛作詐醡笮苲榨𨣮𨢦溠咋 |- | 6:050 || ㅊ || 창 ˧ 蹉嵯磋瑳搓差叉杈靫扠差艖舣鎈 창〯 ˧˦ 瑳 창〮 ˦ 磋蹉差汊衩 |- | 6:051 || ㅉ || 짱 ˧ 醝鹺艖𦪸䑘㽨瘥𣩈嵳𥰭齹䣜鄼 |- | 6:051 || ㅅ || 상 ˧ 娑挱莎莏挲沙䤬𨪍桫髿𣯌犠獻戲傞𠈱沙𣲡砂紗𦀟鯊㲚髿硰 상〯 ˧˦ 娑灑洒葰傻 상〮 ˦ 些娑逤嗄𣣺沙 |- | 6:052 || ㅆ || 쌍 ˧ 槎楂苴 쌍〯 ˧˦ 槎𠞊柞 쌍〮 ˦ 乍䄍蓌 |- | 6:053 || ㆆ || ᅙᅡᆼ ˧ 阿娿痾䋪䋍鴉鴉丫啞亞 ᅙᅡᆼ〯 ˧˦ 娿婀妸❌阿猗啞瘂❌ ᅙᅡᆼ〮 ˦ 亞惡啞稏婭 |- | 6:053 || ㅎ || 항 ˧ 訶苛何呵㰤呀谺岈❌鰕 항〯 ˧˦ 㰤閜 항〮 ˦ 呵罅㙤釁❌❌嚇赫哧謑 |- | 6:054 || ㆅ || ᅘᅡᆼ ˧ 何荷河苛遐徦假蕸霞瑕鍜碬蝦鰕騢假赮 ᅘᅡᆼ〯 ˧˦ 荷何❌抲苛下夏廈 ᅘᅡᆼ〮 ˦ 賀❌荷何暇假嘉下芐夏 |- | 6:055 || ㄹ || 랑 ˧ 羅蘿籮❌鑼欏囉饠 랑〯 ˧˦ 砢❌㦬❌攞邏 |} === ㅑ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:055 || ㄱ || 걍 ˧ 迦 |- | 6:055 || ㅋ || 컁 ˧ 呿 |- | 6:055 || ㄲ || 꺙 ˧ 伽茄 |- | 6:055 || ㅈ || 쟝 ˧ 嗟罝𦋽遮庶 쟝〯 ˧˦ 姐她媎者赭堵 쟝〮 ˦ 借藉唶柘䂞蔗𤯈𤯋鷓蟅䗪炙 |- | 6:056 || ㅊ || 챵 ˧ 車 챵〯 ˧˦ 且奲撦䰩奓哆拸䞣 |- | 6:056 || ㅉ || 쨩〯 ˧˦ 抯飷 쨩〮 ˦ 藉蒩耤 |- | 6:057 || ㅅ || 샹 ˧ 些𡭟奢賖畬 샹〯 ˧˦ 寫𣞐瀉捨舍 샹〮 ˦ 卸瀉舍赦厙 |- | 6:057 || ㅆ || 썅 ˧ 邪耶斜䔑𦳃闍堵蛇虵鉈 썅〯 ˧˦ 䄬社 썅〮 ˦ 謝榭㴬射䠶麝貰 |- | 6:058 || ㅇ || 양 ˧ 邪釾鋣鎁枒椰椰耶爺斜 양〯 ˧˦ 野野也冶蠱 양〮 ˦ 夜射 |- | 6:058 || ㅿ || ᅀᅣᆼ〯 ˧˦ 惹❌若 |} === ㅘ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:058 || ㄱ || 광 ˧ 戈過撾渦❌鍋鈛❌瓜媧騧❌蝸緺 광〯 ˧˦ 果菓惈❌裹蜾裸寡❌另剮 광〮 ˦ 過裹燴 |- | 6:059 || ㅋ || 쾅 ˧ 科蝌窠薖簻誇侉胯跨姱恗夸❌荂華 쾅〯 ˧˦ 顆堁果骻胯跨㡁袴銙❌錁❌夸 쾅〮 ˦ 課堁跨❌夸胯袴❌ |- | 6:060 || ㄲ || 꽝 ˧ 瘸 |- | 6:060 || ㆁ || ᅌᅪᆼ ˧ 吪❌囮繇鈋譌訛 ᅌᅪᆼ〯 ˧˦ 瓦 ᅌᅪᆼ〮 ˦ 臥 |- | 6:060 || ㄷ || 돵 ˧ 檛薖撾 돵〯 ˧˦ 朶揣挅捶㪜崜埵𥠄鬌 |- | 6:061 || ㅌ || 퇑 ˧ 詑 퇑〯 ˧˦ 妥綏𢼻隋橢䲊墮媠 퇑〮 ˦ 唾涶佗扡拕拖大 |- | 6:061 || ㄸ || 뙁〯 ˧˦ 惰嶞墮墯隓𡐦垜𨹃䅜 뙁〮 ˦ 惰憜媠墮𢞑𢢠 |- | 6:062 || ㄴ || 놩 ˧ 捼挼 놩〮 ˦ 愞懦偄𦓏稬糯𤲬堧壖 |- | 6:062 || ㅈ || 좡 ˧ 髽 좡〮 ˦ 挫蓌夎䟶 |- | 6:062 || ㅊ || 촹〯 ˧˦ 脞 촹〮 ˦ 剉挫莝摧 |- | 6:062 || ㅉ || 쫭 ˧ 矬痤 쫭〯 ˧˦ 坐 쫭〮 ˦ 坐座 |- | 6:063 || ㅅ || 솽 ˧ 蓑莎梭❌唆 솽〯 ˧˦ 鎖鏁瑣璅❌葰 |- | 6:063 || ㆆ || ᅙᅪᆼ ˧ 倭踒渦窩窊窳溛窪窐洼❌哇娃蛙䵷汙 ᅙᅪᆼ〯 ˧˦ 婐果 ᅙᅪᆼ〮 ˦ 涴汙堁 |- | 6:064 || ㅎ || 황 ˧ 鞾靴❌❌㞜花華❌ 황〯 ˧˦ 火 황〮 ˦ 貨化 |- | 6:064 || ㆅ || ᅘᅪᆼ ˧ 和龢鉌禾華崋譁嘩驊❌鋘釫鏵 ᅘᅪᆼ〯 ˧˦ 禍❌輠❌夥❌髁踝裸裸觟 ᅘᅪᆼ〮 ˦ 和華樺檴嬅摦擭獲鱯㕦 |- | 6:065 || ㄹ || 뢍 ˧ 鸁䯁騾臝蠃螺蠡蝸❌❌鏍虆蔂❌❌覼 뢍〯 ˧˦ 裸裸果臝儽蠃蓏❌蠡❌瘰攭 뢍〮 ˦ 邏摞 |} === ㅜ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:066 || ㄱ || 궁 ˧ 拘佝句❌跔駒驕痀俱㪺❌仇捄 궁〯 ˧˦ 矩榘距句拒枸蒟萬楀踽偊鄅 궁〮 ˦ 屨鞻句絇蒟瞿懼 |- | 6:067 || ㅋ || 쿵 ˧ 區驅毆敺❌歐嶇軀❌鮬摳 쿵〯 ˧˦ 齲踽 쿵〮 ˦ 驅 |- | 6:067 || ㄲ || 꿍 ˧ 劬朐❌絇句❌❌䋧軥枸鼩翑句臞癯躣躩忂灈懼戵鑺欋衢瞿鸜 꿍〯 ˧˦ 寠窶 꿍〮 ˦ 懼瞿具❌❌ |- | 6:068 || ㆁ || ᅌᅮᆼ ˧ 虞❌澞鸆麌娛禺愚隅嵎髃腢喁齵堣鍝于竽𥫡迂盂❌杅❌玗釪汙邘❌雩❌謣❌ ᅌᅮᆼ〯 ˧˦ 麌❌俁㒁羽禹䨞萭瑀偊宇㝢㡰❌雨 ᅌᅮᆼ〮 ˦ 遇寓庽禺虞芌芋𩁹吁羽雨 |- | 6:070 || ㅂ || 붕 ˧ 膚❌夫玞扶䄮鈇柎不❌枎跗趺附❌枹 붕〯 ˧˦ 甫父莆黼❌脯俌簠❌府腑俯斧鈇 붕〮 ˦ 付傅❌賦富 |- | 6:071 || ㅍ || 풍 ˧ 敷傳鋪尃溥懯孚莩罦䍖❌稃粰❌❌桴俘郛垺𡎽苻荴紨泭柎❌怤荂❌旉華麩麱䴸鄜 풍〯 ˧˦ 撫❌拊柎弣 풍〮 ˦ 赴訃仆踣掊䞳趏簠副報 |- | 6:072 || ㅃ || 뿡 ˧ 扶蚨颫夫❌芙符苻❌鳧瓿 뿡〯 ˧˦ 父㕮駁輔䩉❌鬴釜❌秿❌滏腐焤 뿡〮 ˦ 附胕柎付駙䮛鮒❌柎坿跗傅賻負負萯偩婦 |- | 6:073 || ㅁ || 뭉 ˧ 無亡蕪䍢❌璑毋巫誣 뭉〯 ˧˦ 武鵡母碔珷璑舞儛廡❌膴嫵斌憮㒇嘸甒❌❌橆蕪侮侮務䍙堥 뭉〮 ˦ 務婺瞀騖鶩霧 |- | 6:074 || ㅊ || 충 ˧ 芻䅳 |- | 6:074 || ㅉ || 쭝 ˧ 雛䅳媰 |- | 6:074 || ㅅ || 숭 ˧ 毹毺㲣𡨙氀 숭〮 ˦ 數捒 |- | 6:074 || ㆆ || ᅙᅮᆼ ˧ 紆汙迂盓陓 ᅙᅮᆼ〯 ˧˦ 傴痀❌嫗噢 ᅙᅮᆼ〮 ˦ 嫗饇歐燠噢 |- | 6:075 || ㅎ || 훙 ˧ 訏吁盱❌芋❌旴晇冔❌昫煦蓲姁喣嘔呴謳 훙〯 ˧˦ 詡栩珝訏冔❌姁昫喣煦煦膴咻 훙〮 ˦ 煦昫呴欨咻休䣱酗 |- | 6:076 || ㄹ || 룽 ˧ 慺膢褸漊鏤氀❌蔞𦭯婁 룽〯 ˧˦ 縷僂軁漊嶁㟺褸簍蔞婁 룽〮 ˦ 屢婁僂 |} === ㅠ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:077 || ㄷ || 듕 ˧ 株誅蛛跦邾禂 듕〯 ˧˦ 𪐴拄柱 듕〮 ˦ 駐軴柱住𨙦𩦛 |- | 6:077 || ㅌ || 튱 ˧ 貙 |- | 6:077 || ㄸ || 뜡 ˧ 廚躕裯幮幬趎 뜡〯 ˧˦ 柱 뜡〮 ˦ 住 |- | 6:078 || ㅈ || 즁 ˧ 諏𧩻娵朱珠侏咮祩袾 즁〯 ˧˦ 主宔砫袾麈炷枓斗 즁〮 ˦ 足注屬主註炷鉒䪒澍霔咮𠰍鑄馵 |- | 6:078 || ㅊ || 츙 ˧ 趨𨃘趣騶取樞姝 츙〯 ˧˦ 取 츙〮 ˦ 娶趣趨 |- | 6:079 || ㅉ || 쯍〯 ˧˦ 聚 쯍〮 ˦ 聚鄹𡒍㝡 |- | 6:079 || ㅅ || 슝 ˧ 須𩓣𥪙𥪥䇕𡡓鬚需繻緰㡏𦅎𪋯輸隃鄃 슝〯 ˧˦ 數籔簍藪 슝〮 ˦ 戍隃輸束 |- | 6:080 || ㅆ || 쓩 ˧ 殊銖洙㼡茱殳杸 쓩〯 ˧˦ 豎𠐊侸裋𧞫樹 쓩〮 ˦ 樹澍裋𧞫 |- | 6:080 || ㅇ || 융 ˧ 兪逾踰𧼯窬瘉瘐𨵦渝楡揄𦩞羭蝓𧍳堬瑜牏褕喩愉婾愈睮覦歈㼶臾㬰諛楰腴㥚萸 융〯 ˧˦ 庾㢏㔱楰㥚斞斔愉瘐瘉臾愈瘉癒貐窳寙 융〮 ˦ 裕䘱欲慾籲諭喩覦兪瘉 |- | 6:082 || ㅿ || ᅀᅲᆼ ˧ 儒𠍶嚅吺咮懦愞濡𣽉醹襦𧝄 ᅀᅲᆼ〯 ˧˦ 乳醹㳶 ᅀᅲᆼ〮 ˦ 孺𡦗乳 |} === ㅓ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:083 || ㄱ || 겅 ˧ 居㞐琚❌裾据椐鶋❌車 겅〯 ˧˦ 擧莒筥❌籧❌柜欅弆 겅〮 ˦ 據鐻踞倨裾鋸據居擧 |- | 6:083 || ㅋ || 컹 ˧ 墟丘嶇㠊袪祛佉胠阹呿抾魼鱋去 컹〯 ˧˦ 去麮胠 컹〮 ˦ 去㰦呿胠 |- | 6:084 || ㄲ || 껑 ˧ 渠蕖❌❌❌磲❌蘧遽籧鐻❌璩璖❌醵䣰腒 껑〯 ˧˦ 巨鉅詎渠距❌❌拒歫蚷駏❌炬❌秬岠怇苣虡簴❌❌鐻 껑〮 ˦ 遽蘧懅醵䣰勮詎巨渠 |- | 6:085 || ㆁ || ᅌᅥᆼ ˧ 魚䁩❌䐳❌漁䱷䰻齬衙 ᅌᅥᆼ〯 ˧˦ 語齬鋙❌峿敔梧衙圄圉禦御蘌❌❌❌ ᅌᅥᆼ〮 ˦ 御御禦衙圉語䱷漁 |- | 6:086 || ㆆ || ᅙᅥᆼ ˧ 於淤唹 ᅙᅥᆼ〮 ˦ 飫饇❌❌棜淤❌瘀菸 |- | 6:086 || ㅎ || 헝 ˧ 虛虗歔噓吁喣呴驉❌魖 헝〯 ˧˦ 許 헝〮 ˦ 噓 |} === ㅕ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:087 || ㄷ || 뎡 ˧ 豬猪䐗櫫瀦潴 뎡〯 ˧˦ 貯𡪄著紵䘢宁 뎡〮 ˦ 著 |- | 6:087 || ㅌ || 텽 ˧ 攄捈樗櫖摴摢㻬 텽〯 ˧˦ 楮柠褚 텽〮 ˦ 絮 |- | 6:088 || ㄸ || 뗭 ˧ 除篨滁筡著躇屠儲宁 뗭〯 ˧˦ 宁佇竚紵𦂂羜泞眝坾杼拧䇡抒苧 뗭〮 ˦ 箸櫡躇著宁除 |- | 6:088 || ㄴ || 녕 ˧ 袽帤挐拏 녕〯 ˧˦ 女 녕〮 ˦ 女 |- | 6:089 || ㅈ || 졍 ˧ 苴蒩且蛆沮諸䃴蠩 졍〯 ˧˦ 苴疽䰞𩱰𤑨煮渚陼庶 졍〮 ˦ 怚姐𢚆沮苴翥䬡庶 |- | 6:089 || ㅊ || 쳥 ˧ 疽砠❌沮趄跙狙雎苴 쳥〯 ˧˦ 且杵処處 쳥〮 ˦ 覰䁦狙蜡處 |- | 6:090 || ㅉ || 쪙〯 ˧˦ 沮咀跙 |- | 6:090 || ㅅ || 셩 ˧ 胥諝㥠胥糈稰蝑湑書𦘠舒豫𪅰紓䋡悆忬瑹荼 셩〯 ˧˦ 諝醑湑稰胥暑黍鼠癙 셩〮 ˦ 絮恕庶 |- | 6:091 || ㅆ || 쎵 ˧ 徐邪蜍蠩 쎵〯 ˧˦ 敍漵㵰序䦽㘧茅𣏗杼藇𨣦鱮魣嶼緖紓墅野杼茅𣏗桃 쎵〮 ˦ 署𥌓暏 |- | 6:092 || ㅇ || 영 ˧ 余予畬❌畭蜍餘與歟欤懙❌㦛旟璵㼂譽鸒輿❌舁伃妤茅雓 영〯 ˧˦ 與❌歟予❌ 영〮 ˦ 豫舒余與鸒譽礜藇蕷穥歟輿預忬澦❌悆 |- | 6:093 || ㄹ || 령 ˧ 臚❌盧膚驢䮫廬慮藘閭櫚 령〯 ˧˦ 呂呂侶梠膂旅❌祣❌臚穭稆儢❌ 령〮 ˦ 慮爈❌䥨鋁❌濾勴錄窶 |- | 6:094 || ㅿ || ᅀᅧᆼ ˧ 如鴐茹絮洳 ᅀᅧᆼ〯 ˧˦ 汝籹❌女茹 ᅀᅧᆼ〮 ˦ 茹洳如 |} {{단원 안내|책=동국정운 색인|상위=동국정운 색인 |이전=끝소리 ㅱ |다음=끝소리 ㆁ}} [[분류:동국정운]] nnhe2muw8zto9a8677b3ns2ml0721un 45124 45123 2024-10-30T18:19:24Z 2600:1700:C76:9010:22A7:9C6B:AF28:E81F /* ㅘ */ 45124 wikitext text/x-wiki === ㆍ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:008 || ㅈ || ᄌᆞᆼ ˧ 貲訾𤺒訿觜髭𩑽鄑咨諮資䆅姿粢秶澬姕𪗋齊𧞓齍璾齎玆茲孳滋嵫鎡仔孜耔鼒 ᄌᆞᆼ〯 ˧˦ 紫訿𤺒啙呰訾跐㧗姊秭子耔芓仔杍梓榟 ᄌᆞᆼ〮 ˦ 恣積𥡯㧘柴 |- | 5:009 || ㅊ || ᄎᆞᆼ ˧ 雌䳄郪趑次𨀥𨒮❌ ᄎᆞᆼ〯 ˧˦ 此泚佌𠈈𢇌玼皉 ᄎᆞᆼ〮 ˦ 刺𧧒庛❌𢉪次佽㐸絘䯸 |- | 5:010 || ㅉ || ᄍᆞᆼ ˧ 慈㤵磁鶿鷀鴜茲茨餈𩜴𥻓粢䭣薋𩆂䨏𩆃瓷❌薺疵玼骴髊𩨱茈𦶉訾 ᄍᆞᆼ〮 ˦ 字牸孶自漬眥眦骴髊㱴餗 |- | 5:011 || ㅅ || ᄉᆞᆼ ˧ 私思罳䰄偲愢緦颸絲司伺覗𥄶斯撕澌凘𪆁廝㒋虒傂禠㴲磃𩆵菥析徙師獅𤜳篩釃𦌿灑廝襹褷籭簛簁蓰 ᄉᆞᆼ〯 ˧˦ 徙璽死枲葸𤟧諰鰓禗躧蹝屣釃纚縰灑洒矖𥊂𩌦𩎉蓰簁籭漇史駛使 ᄉᆞᆼ〮 ˦ 賜錫瀃澌𣩠杫𣘩㮐四泗柶駟肆𩬶肄笥伺覗司僿思𩢲駛使𩌦屣灑洒曬釃䚕 |- | 5:013 || ㅆ || ᄊᆞᆼ ˧ 詞祠柌辭辤漦 ᄊᆞᆼ〯 ˧˦ 兕𧰽𠒃似仏姒㚶耜梩𦓨杞䎣枱𨐠祀𥘰𧝀汜巳士仕𣐈戺俟竢𥏳𢓪䇃𢈟𢉡逘𣀼𡱢涘騃 ᄊᆞᆼ〮 ˦ 寺嗣飤飼食飴事 |} === ㅣ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:014 || ㅋ || 킹〯 ˧˦ 企跂𠈮 킹〮 ˦ 棄企❌跂蚑 |- | 5:015 || ㄲ || 낑 ˧ 祇軝❌軙忯疧疷伎跂枝蚑𨙸岐歧祁耆鰭愭鬐 |- | 5:015 || ㄷ || 딩 ˧ 知𥎿䝷蜘𧐉鼅胝疷䟡躓𦙁 딩〯 ˧˦ 徵黹𧝉䌤緻絺希鵗 딩〮 ˦ 致致輊輖𦥎䡹質劕躓懫懥驇疐𨇈嚔❌𢷟智知置 |- | 5:016 || ㅌ || 팅 ˧ 摛攡螭彲離𡖟魑离𣉽絺瓻癡𢣕𠈴笞抬𪗪呞 팅〯 ˧˦ 褫搋拸䚦扡扯恥誀祉 팅〮 ˦ 眙 |- | 5:017 || ㄸ || 띵 ˧ 馳䮈池篪竾筂褫簃𠗺謻趨邸踟踶墀𡎰遲遟𨒈赿❌䜄坻汣𡊆沶泜蚳𧐏貾治耛菭持蛇 띵〯 ˧˦ 豸𧋈褫傂廌阤陁陊杝雉鴙鶨薙埃垁滍泜踶峙𠍰跱畤庤𤲵痔偫崻 띵〮 ˦ 地坔埊緻致穉稚䆈䄺䕌謘遲遟治値直植置褫彘 |- | 5:019 || ㄴ || 닝 ˧ 尼㞾怩跜秜旎 닝〯 ˧˦ 柅旎狔 닝〮 ˦ 膩𦡸 |- | 5:020 || ㅂ || 빙 ˧ 卑庳箄裨陴埤鞞錍椑陂波詖羆❌❌罷藣❌❌碑悲非餥裴蜚誹緋扉飛騛 빙〯 ˧˦ 俾卑髀脾鞞箄匕比妣䃾❌秕粃❌朼枇彼佊鄙啚否匪篚榧棐蜚奜蜰 빙〮 ˦ 臂嬖辟畀❌痹比庛芘賁跛詖陂披佊貱波藣祕秘泌柲鉍䪐閟毖鄪費❌粊轡䩛沸髴❌誹非芾茀 |- | 5:022 || ㅍ || 핑 ˧ 紕❌❌悂鈹鉟披㱟㨢被旇翍耚❌丕㔻伾秠怌駓䮆豾狉銔批霏䬠菲騑匪❌馡裶裴妃婓❌ 핑〯 ˧˦ 庀比庇疕仳諀吡批披㱟噽秠❌斐菲誹非悱朏 핑〮 ˦ 譬辟濞淠帔被襬費 |- | 5:024 || ㅃ || 삥 ˧ 毗紕辟𨈚❌❌阰❌魮❌鈚蚍螕琵批比膍肶㮰貔豼❌❌陴埤脾裨崥皮疲罷郫邳岯坯❌魾肥淝腓痱❌厞婓賁 삥〯 ˧˦ 婢埤䠋卑庳埤否痞岯圮䤏被骳罷陫❌ 삥〮 ˦ 鼻比紕枇𢈷庳卑避辟髲被鞁骳備俻犕糒奰贔屝厞陫❌痱腓菲蜚翡䠊剕❌費狒𥜿𥝃𦦻𤲳昲𪎰黂䆊 |- | 5:027 || ㅁ || 밍 ˧ 彌弥𢏏㜷𡝠瀰冞𥹐糜𩞁𩞇𪎭穈𪐎縻𥿫𦄐䌕靡㸏𤓒𦗕劘𪎕𠞧醾𨣿醿蘼蘪攠❌眉嵋湄𣽪𤃰攗楣郿媚麋麛黴微𧗬薇㵟溦霺 밍〯 ˧˦ 弭渳彌瀰𣴱沔敉侎𢘺芉美渼媺靡𦗕尾亹斖娓 밍〮 ˦ 寐媢媚郿篃魅靡未味 |- | 5:028 || ㅈ || 징 ˧ 支枝肢𨈪𨈙跂䧴鳷巵觶𧣨梔氏祇祗衹褆㩼多秖榰禔疧脂祗砥厎泜之芝 징〯 ˧˦ 紙帋坁汦汣❌抵觗㧗抵砥只𠮡軹枳咫𦐖❌疻旨𣅀指𢫾脂恉厎底𦒿止芷茝沚洔阯址趾畤 징〮 ˦ 寘忮伎觶𧣨觗至摯贄質鷙𪁊礩志誌識痣䏯織綕𥿮幟𢂴笫 |- | 5:031 || ㅊ || 칭 ˧ 蚩嗤𢾫媸鴟鵄眵 칭〯 ˧˦ 侈奓鉹𨪐誃哆恀䏧姼𡚼㶴袳移𧛧袲㢋㢁懘齒茝䊼 칭〮 ˦ 熾𧹹幟織旘𢂴志饎糦喜𩛉𩜮𩜂埴𡑌戠 |- | 5:032 || ㅅ || 싱 ˧ 施葹鍦鉇𥍸𥍢絁䌤𦂛䙾𧠜𧠉翅尸㕧鳲𨾋䲩屍蓍詩邿 싱〯 ˧˦ 弛𢐋㢮阤施豕矢笑屎始 싱〮 ˦ 翅𦐊翄翨𦑧施鍦釶啻翅試式弑殺煞𢎍識幟織始失 |- | 5:033 || ㅆ || 씽 ˧ 茬茌匙㮛堤提𦑡禔媞鍉時塒鰣 씽〯 ˧˦ 是諟惿媞𨸝䟗氏視市恃㤄𦧇舐𦧧咶狧 씽〮 ˦ 示謚𧨦豉嗜耆𩝙𨢍𦞯呩視侍寺䦙蒔 |- | 5:034 || ㆆ || ᅙᅵᆼ ˧ 伊洢咿吚黟黝❌ ᅙᅵᆼ〮 ˦ 縊 |- | 5:035 || ㅎ || 힝 ˧ 㕧屎䐖❌❌❌ |- | 5:035 || ㅇ || 잉 ˧ 移拖❌䔟簃❌扅❌栘鉹袲椸箷䈕㮛❌暆貤酏匜詑訑施❌❌蛇夷尼❌痍荑恞𢓡跠峓鐵侇桋䧅姨洟❌胰寅夤彛飴❌❌❌䬮怡眙詒❌貽瓵怠台頤宧圯異 잉〯 ˧˦ 以已苢苡酏祂肔胣匜迆迤施崺❌ 잉〮 ˦ 易㑥敡施袘❌緆衪貤❌移肄肆勩❌廙異異食詒貽 |- | 5:037 || ㄹ || 링 ˧ 離蘺籬杝㰚㒿䍦❌灕漓離攡摛褵縭璃䅻醨离鸝鵹❌❌❌矖纚驪孋穲䕻❌麗罹蠡盠❌劙❌棃❌棃梨犂黧❌犂❌❌犁蔾❌菞釐剺❌氂犛狸剺嫠來倈徠䅘貍狸❌猍梩❌ 링〯 ˧˦ 里理鯉❌俚悝裏李邐麗履 링〮 ˦ 詈茘離离利莅蒞位吏 |- | 5:040 || ㅿ || ᅀᅵᆼ ˧ 兒唲而胹臑腝❌❌洏聏陑栭鮞髵耏耐鴯怠❌‗ 濡 ᅀᅵᆼ〯 ˧˦ 爾爾爾邇邇邇耳耳珥駬䋙餌柅鑈檷 ᅀᅵᆼ〮 ˦ 二貳㒃樲餌㢽❌珥鉺衈咡刵毦髶聏 |} === ㆎ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:041 || ㄷ || ᄃᆡᆼ〮 ˦ 戴載襶 |- | 5:041 || ㅌ || ᄐᆡᆼ ˧ 胎孡台能駘鮐邰斄釐漦 ᄐᆡᆼ〮 ˦ 貸態詒紿怠䈚箈殆待 |- | 5:041 || ㄸ || ᄄᆡᆼ ˧ 臺𡌬薹籉儓擡鮐駘跆苔炱炲䈚箈 ᄄᆡᆼ〯 ˧˦ 待𥩳逮迨隶𨽿駘紿詒殆怠䈚箈靆 ᄄᆡᆼ〮 ˦ 代岱袋黛逮曃靆棣埭𥓏𡍖瑇❌毒玳 |- | 5:043 || ㅂ || ᄇᆡᆼ ˧ 杯盃𦈶𦈧𠤯桮 ᄇᆡᆼ〮 ˦ 背輩軰 |- | 5:043 || ㅍ || ᄑᆡᆼ ˧ 胚妚衃阫坏培醅❌ ᄑᆡᆼ〯 ˧˦ 朏 ᄑᆡᆼ〮 ˦ 配妃朏 |- | 5:043 || ㅃ || ᄈᆡᆼ ˧ 裴徘俳培陪倍阫 ᄈᆡᆼ〯 ˧˦ 琲㻗痱❌倍 ᄈᆡᆼ〮 ˦ 佩佩背偝負倍㔨北邶鄁倍焙❌孛茀勃誖悖哱怫㫲琲㻗拔萯 |- | 5:044 || ㅁ || ᄆᆡᆼ ˧ 枚玫梅楳某槑❌鋂脢脄❌膴酶䊈莓每䍙禖媒煤塺 ᄆᆡᆼ〯 ˧˦ 浼每痗脢脄 ᄆᆡᆼ〮 ˦ 妹昧㫚眛沬韎❌每痗脢脄沕琩❌冒媒 |- | 5:046 || ㅈ || ᄌᆡᆼ ˧ 哉裁栽𦳦載災灾菑甾𤆄 ᄌᆡᆼ〯 ˧˦ 宰䏁縡載 ᄌᆡᆼ〮 ˦ 再載縡 |- | 5:046 || ㅊ || ᄎᆡᆼ ˧ 猜偲 ᄎᆡᆼ〯 ˧˦ 采採寀彩棌綵䰂茝 ᄎᆡᆼ〮 ˦ 菜寀采縩 |- | 5:046 || ㅉ || ᄍᆡᆼ ˧ 裁才材財鼒纔 ᄍᆡᆼ〯 ˧˦ 在 ᄍᆡᆼ〮 ˦ 在裁栽載酨 |- | 5:047 || ㅅ || ᄉᆡᆼ ˧ 鰓䚡䰄思罳❌顋毸 ᄉᆡᆼ〮 ˦ 塞僿簺賽 |- | 5:047 || ㆆ || ᅙᆡᆼ ˧ 哀埃㶼欸唉 ᅙᆡᆼ〯 ˧˦ 欸唉款靉靄藹娭毐 ᅙᆡᆼ〮 ˦ 愛靉曖僾薆藹靄欸 |- | 5:048 || ㅎ || ᄒᆡᆼ ˧ 咍 ᄒᆡᆼ〯 ˧˦ 海❌醢❌ |- | 5:048 || ㆅ || ᅘᆡᆼ ˧ 孩㜾❌頦 ᅘᆡᆼ〯 ˧˦ 亥侅劾 ᅘᆡᆼ〮 ˦ 瀣劾 |- | 5:048 || ㄹ || ᄅᆡᆼ ˧ 來逨❌倈萊徠䅘❌騋淶崍 ᄅᆡᆼ〮 ˦ 徠倈來睞賚萊 |} === ㅢ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:049 || ㄱ || 긩 ˧ 羈䩭鞿羇踦奇倚踦奇畸觭掎剞飢饑❌肌❌姬箕❌❌箕諆❌錤棋檱踑其基朞期祺居機鐖璣禨嘰饑鞿磯譏耭蟣幾刏 긩〯 ˧˦ 己紀几機㲹麂掎踦剞❌庋攱庪㨳蟣❌穖幾 긩〮 ˦ 寄徛❌冀兾驥❌懻❌穊❌穖覬幾洎記其忌已㤅近旣漑曁 |- | 5:051 || ㅋ || 킝 ˧ 敧崎觭踦䗁徛碕榿欺倛僛❌娸❌❌❌ 킝〯 ˧˦ 綺觭起杞梩❌㞯玘芑豈䔇 킝〮 ˦ 器❌器亟氣❌乞❌ |- | 5:052 || ㄲ || 끵 ˧ 奇騎錡琦碕埼隑其萁稘期琪淇祺麒騏鶀❌旗棊碁櫀蜝綦綨❌❌𤪌璂蘄祈❌蘄肵旂頎畿圻幾❌刏❌機碕 끵〯 ˧˦ 技伎妓䗁錡跽❌ 끵〮 ˦ 芰茤騎䗁妓技❌洎垍曁❌墍蔇忌鵋誋惎㥍諅綦禨璣幾曁刏 |- | 5:054 || ㆁ || ᅌᅴᆼ ˧ 宜義儀❌檥轙議鸃䴊㕒嶬涯崖疑儗嶷觺沂凒皚溰 ᅌᅴᆼ〯 ˧˦ 蟻蟻蛾❌❌轙艤檥礒硪齮錡擬譺懝儗疑薿❌孴❌矣顗 ᅌᅴᆼ〮 ˦ 義誼❌竩議儗❌毅藙 |- | 5:055 || ㅈ || 즹 ˧ 菑載𤰭䅔𦿨淄甾緇純䊷輜椔錙鶅𨿴甾 즹〯 ˧˦ 滓胏𦞤𦙰笫緇 즹〮 ˦ 胾椔緇菑倳事輜剚𨧫 |- | 5:056 || ㅊ || 칑 ˧ 差嵯嵳䡨𨍃 칑〮 ˦ 廁厠 |- | 5:056 || ㆆ || ᅙᅴᆼ ˧ 漪猗䝝兮椅欹旖禕醫毉噫意億懿嘻譆❌衣依譩 ᅙᅴᆼ〯 ˧˦ 倚奇椅檹輢猗旖㫊䭲譩醫醷臆旖㕈庡依偯 ᅙᅴᆼ〮 ˦ 意薏倚猗輢懿饐❌饖撎衣䵝 |- | 5:058 || ㅎ || 힁 ˧ 犧曦㬢爔羲戲嚱❌❌❌❌檥巇嶬桸獻僖嬉嘻禧釐譆熹暿熺熙娭㜯稀俙晞睎豨狶欷唏鵗希 힁〯 ˧˦ 喜憘憙嬉豨唏俙霼 힁〮 ˦ 戲齂哂嚊屭豷燹咥憙喜憘嬉欷唏愾餼䊠旣熂霼墍❌❌摡❌黖 |} === ㅚ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:060 || ㄱ || 굉 ˧ 傀❌❌❌❌瑰瓌璝 굉〮 ˦ 儈會襘檜澮膾鱠獪䯤鬠❌旝鄶劊會廥憒慖幗蔮刏 |- | 5:061 || ㅋ || 쾽 ˧ 恢❌詼盔魁❌悝 쾽〮 ˦ ❌塊蕢蒯❌堁 |- | 5:061 || ㆁ || ᅌᅬᆼ ˧ 嵬峞隗阢桅磑 ᅌᅬᆼ〯 ˧˦ 隗嵬峞䫥頠 ᅌᅬᆼ〮 ˦ 外磑 |- | 5:061 || ㄷ || 됭 ˧ 鎚追❌頧搥槌磓堆❌塠㕍❌❌敦 됭〮 ˦ 祋杸對轛❌碓敦 |- | 5:062 || ㅌ || 툉 ˧ 推蓷焞 툉〯 ˧˦ 腿 툉〮 ˦ 娧脫稅駾蛻兌退𨑧𢓇 |- | 5:063 || ㄸ || 뙹 ˧ 隤頹墤穨尵僓魋 뙹〯 ˧˦ 鐓憝隊 뙹〮 ˦ 兌𩊭銳𠜑奪隊䨴𩅥𩄮薱憝譈懟憞譵鐓駾 |- | 5:063 || ㄴ || 뇡 ˧ 挼捼 뇡〯 ˧˦ 餧餒鯘鮾脮腇腇 뇡〮 ˦ 內 |- | 5:064 || ㅈ || 죙〮 ˦ 最蕞棷粹晬綷❌祽 |- | 5:064 || ㅊ || 쵱 ˧ 崔催縗衰䙑 쵱〯 ˧˦ 漼璀皠洒綷萃 쵱〮 ˦ 襊㝮竄倅萃淬焠𠗚啐啛 |- | 5:065 || ㅉ || 쬥 ˧ 摧漼凗崔㠑磪𡹐 쬥〯 ˧˦ 辠𡽕 쬥〮 ˦ 蕞 |- | 5:065 || ㅅ || 쇵 ˧ 𣯧𦸏挼䪎鞖 쇵〮 ˦ 碎粹誶 |- | 5:065 || ㆆ || ᅙᅬᆼ ˧ 隈❌椳煨䋿偎畏 ᅙᅬᆼ〯 ˧˦ 猥❌椳萎 ᅙᅬᆼ〮 ˦ 薈懀濊 |- | 5:065 || ㅎ || 횡 ˧ 灰❌䝇豗拻 횡〯 ˧˦ 賄悔 횡〮 ˦ ❌噦鐬翽誨晦悔❌靧頮 |- | 5:066 || ㆅ || ᅘᅬᆼ ˧ 回廻茴洄徊瑰槐瘣 ᅘᅬᆼ〯 ˧˦ 瘣壞廆匯滙回 ᅘᅬᆼ〮 ˦ 會禬繪䙡璯潰繪繪䜋聵瞶闠回匯䔇 |- | 5:067 || ㄹ || 룅 ˧ 雷䨓❌罍❌鑘𨯔❌礧轠瓃❌ 룅〯 ˧˦ 磊磥礌礧❌㠥礨壘櫑儡❌❌❌㒦❌蕾 룅〮 ˦ 酹類耒礧壘礌雷㵢攂 |} === ㅐ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:068 || ㄱ || 갱 ˧ 佳街皆偕湝階堦楷喈❌荄痎䕸稭祴該垓畡陔閡絯胲侅晐峐剴祴裓 갱〯 ˧˦ 解改胲❌陔閡 갱〮 ˦ 蓋蓋匃匄丐懈解繲廨解戒誡悈介界堺❌魪玠琾㠹价尬芥疥蚧❌䯰❌屆漑摡槪扢槪㧉剴 |- | 5:070 || ㅋ || 캥 ˧ 揩❌❌❌開 캥〯 ˧˦ 鍇楷愷凱豈鎧塏闓 캥〮 ˦ 磕礚愒❌渴嘅❌揩慨鎧闓欬咳愾 |- | 5:070 || ㆁ || ᅌᅢᆼ ˧ 厓崖涯漄睚倪皚❌凒敱 ᅌᅢᆼ〯 ˧˦ 騃 ᅌᅢᆼ〮 ˦ 艾❌㣻礙硋㝵儗閡 |- | 5:071 || ㄷ || 댕〮 ˦ 帶㿃蔕蹛遞 |- | 5:071 || ㅌ || 탱〮 ˦ 泰太太大忕㥭汰汏蠆𧀱蔕慸 |- | 5:071 || ㄸ || 땡〯 ˧˦ 廌豸豸 땡〮 ˦ 大軑釱汏 |- | 5:072 || ㄴ || 냉 ˧ 能 냉〯 ˧˦ 嬭㛋㚷乃迺鼐 냉〮 ˦ 柰耐𦓎能𣉘奈鼐 |- | 5:072 || ㅂ || 뱅〯 ˧˦ 擺❌捭 뱅〮 ˦ 貝狽伂沛拜扒敗 |- | 5:072 || ㅍ || 팽〮 ˦ 霈沛肺浿派湃㵒 |- | 5:073 || ㅃ || 뺑 ˧ 牌❌❌蠯螷❌排俳 뺑〯 ˧˦ 罷 뺑〮 ˦ 旆沛茷❌柭軷粺❌稗❌❌韛❌❌排糒敗唄 |- | 5:073 || ㅁ || 맹 ˧ 埋貍霾䨪 맹〯 ˧˦ 買 맹〮 ˦ 眛昧沬賣韎霾邁❌勱佅 |- | 5:074 || ㅈ || 쟁 ˧ 齋齊❌ 쟁〯 ˧˦ 跐 쟁〮 ˦ 債責瘵祭 |- | 5:074 || ㅊ || 챙 ˧ 釵靫扠搋差䡨 챙〮 ˦ 蔡❌縩瘥差衩涇 |- | 5:075 || ㅉ || 쨍 ˧ 柴祡豺犲儕 쨍〮 ˦ 眦❌疵眥砦柴 |- | 5:075 || ㅅ || 생 ˧ 簁篩 생〯 ˧˦ 灑洒躧蹝纚徙 생〮 ˦ 曬灑洒鎩铩殺閷煞 |- | 5:076 || ㆆ || ᅙᅢᆼ ˧ 娃哇❌挨唲 ᅙᅢᆼ〯 ˧˦ 矮㾨躷 ᅙᅢᆼ〮 ˦ 藹靄䨠馤壒❌濭隘阨搤噫❌呃嗄餲喝 |- | 5:076 || ㆅ || ᅘᅢᆼ ˧ 諧湝骸膎鞵鞋鮭 ᅘᅢᆼ〯 ˧˦ 蟹蟹獬❌鮭嶰澥解駭絯豥駴夥 ᅘᅢᆼ〮 ˦ 害妎邂解械❌薤❌瀣齘 |- | 5:077 || ㄹ || 랭〮 ˦ 賴瀨籟藾癩襰厲糲蠣賚 |} === ㅙ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:077 || ㄱ || 괭 ˧ 乖❌媧緺騧❌蝸 괭〯 ˧˦ 掛 괭〮 ˦ 卦掛挂絓罣詿怪怪壞夬澮獪㹟狤 |- | 5:078 || ㅋ || 쾡 ˧ ❌華䦱 쾡〮 ˦ 蒯❌蕢塊喟嘳㕟快噲 |- | 5:078 || ㆁ || ᅌᅫᆼ〮 ˦ 聵❌❌ |- | 5:078 || ㅊ || 쵕〮 ˦ 嘬 |- | 5:078 || ㆆ || ᅙᅫᆼ ˧ 蛙鼃洼 |- | 5:078 || ㆅ || ᅘᅫᆼ ˧ 懷櫰槐褢❌❌淮 ᅘᅫᆼ〮 ˦ 壞畵罫繣澅絓話❌躗吳 |} === ㅟ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:079 || ㄱ || 귕 ˧ 嬀潙佹龜騩歸 귕〯 ˧˦ 軌軌軌匭氿漸宄❌晷簋詭垝陒佹❌祪䤥庪庋攱傀鬼 귕〮 ˦ 媿愧❌聭騩攰庪貴瞶劌劂❌撅橛蹶❌❌鱖鱥 |- | 5:080 || ㅋ || 큉 ˧ 虧𧇾𩏣蘬巋 큉〯 ˧˦ 巋蘬 큉〮 ˦ 喟嘳㕟䙡巋缺 |- | 5:081 || ㄲ || 뀡 ˧ 逵㙺夔犪騤戣鍨頯頄馗 뀡〯 ˧˦ 跪 뀡〮 ˦ 匱鐀櫃饋歸蕢䕚簣籄㙺 |- | 5:081 || ㆁ || ᅌᅱᆼ ˧ 危峗峞帷爲韋幃褘湋違闈圍巍嵬犩❌ ᅌᅱᆼ〯 ˧˦ 頠姽峗瓦韙偉瑋煒颹暐韡❌葦 ᅌᅱᆼ〮 ˦ 僞胃❌謂渭蝟猬❌㥜媦緯圍彙❌❌魏犩❌衛熭❌❌轊䡺❌❌彘㻰 |- | 5:082 || ㄷ || 뒹〮 ˦ 轛 |- | 5:082 || ㆆ || ᅙᅱᆼ ˧ 逶倭䴧蜲痿萎委威葳蝛 ᅙᅱᆼ〯 ˧˦ 委骩磈嵬 ᅙᅱᆼ〮 ˦ 餧❌委骩尉慰蔚霨罻畏威 |- | 5:083 || ㅎ || 휭 ˧ 麾戲摩撝暉煇煒輝揮楎椲翬❌褘徽幑㫎 휭〯 ˧˦ 毁毁燬䅏❌毇❌烜䃣蟲虺蘬❌❌燬卉 휭〮 ˦ 毁諱卉 |- | 5:084 || ㅇ || 윙〯 ˧˦ 蔿蘤䦱❌❌ 윙〮 ˦ 位爲 |} === ㆌ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:084 || ㄱ || ᄀᆔᆼ ˧ 規槼摫瞡❌雉鳺 ᄀᆔᆼ〯 ˧˦ 癸湀 ᄀᆔᆼ〮 ˦ 季 |- | 5:084 || ㅋ || ᄏᆔᆼ ˧ 闚窺 ᄏᆔᆼ〯 ˧˦ 跬頃蹞窺❌頍頯 |- | 5:085 || ㄲ || ᄁᆔᆼ ˧ 葵楑鄈 ᄁᆔᆼ〯 ˧˦ 揆 ᄁᆔᆼ〮 ˦ 悸 |- | 5:085 || ㄷ || ᄃᆔᆼ ˧ 腄追䨨 |- | 5:085 || ㄸ || ᄄᆔᆼ ˧ 椎桘鎚錘槌甀魋椎鬌 ᄄᆔᆼ〮 ˦ 縋䋘槌膇㾽硾倕磓墜錘腄甀墜隊隧❌懟譵 |- | 5:086 || ㄴ || ᄂᆔᆼ〮 ˦ 諉 |- | 5:086 || ㅈ || ᄌᆔᆼ ˧ 劑檇㰎崔墔隹崒❌厜隹萑鵻騅錐 ᄌᆔᆼ〯 ˧˦ 觜❌嘴❌嶉捶棰錘箠菙 ᄌᆔᆼ〮 ˦ 醉檇雋惴諈錘 |- | 5:087 || ㅊ || ᄎᆔᆼ ˧ 吹龡炊推蓷衰 ᄎᆔᆼ〯 ˧˦ 趡揣 ᄎᆔᆼ〮 ˦ 翠臎吹䶴龡出 |- | 5:087 || ㅉ || ᄍᆔᆼ〮 ˦ 萃瘁悴顇踤 |- | 5:087 || ㅅ || ᄉᆔᆼ ˧ 綏浽荽荾䒘雖睢濉衰榱 ᄉᆔᆼ〯 ˧˦ 髓髓❌䯝䯝❌膸瀡靃❌巂嶲水準 ᄉᆔᆼ〮 ˦ 邃粹睟誶祟帥帨率 |- | 5:087 || ㅆ || ᄊᆔᆼ ˧ 隨隋遺垂陲倕❌腄誰唯譙脽 ᄊᆔᆼ〯 ˧˦ 菙❌ ᄊᆔᆼ〮 ˦ 遂燧❌鐩澻㴚襚❌璲繸❌❌隧❌隊旞❌❌彗篲穗❌穟瑞睡倕 |- | 5:089 || ㆆ || ᅙᆔᆼ〮 ˦ 恚烓 |- | 5:089 || ㅎ || ᄒᆔᆼ ˧ 墮❌❌隳隋綏觽鐫鐫睢倠 ᄒᆔᆼ〮 ˦ 睢隋綏墮挼侐 |- | 5:090 || ㅇ || ᄋᆔᆼ ˧ 惟維唯濰遺壝蠵❌❌觿 ᄋᆔᆼ〯 ˧˦ 洧鮪痏䔺❌芛芟唯踓趡壝 ᄋᆔᆼ〮 ˦ 遺❌壝瞶蜼 |- | 5:090 || ㄹ || ᄅᆔᆼ ˧ 羸纍縲累欙樏㹎❌虆藟❌蘲蔂絫 ᄅᆔᆼ〯 ˧˦ 壘藟虆蘽櫐讄❌❌❌礧❌㠥❌礨礨❌蠝❌累樏誄蜼猚 ᄅᆔᆼ〮 ˦ 類禷蘱率淚累絫 |- | 5:092 || ㅿ || ᅀᆔᆼ ˧ 甤蕤苼痿緌綏䅑䅗桵㮃 ᅀᆔᆼ〯 ˧˦ 繠橤蘂❌蕊 |} === ㅖ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:008 || ㄱ || 곙 ˧ 雞稽𮃛笄䈕枅𣓖卟乩 곙〮 ˦ 罽𦇧𣯅𦆡𣽄猘狾計繼㡭繫毄係❌髻結𨜒薊 |- | 6:008 || ㅋ || 콍 ˧ 谿溪磎嵠鸂䳶 콍〯 ˧˦ 啓棨𢧊綮启稽 콍〮 ˦ 愒憇𢠾偈❌揭䔾甈契栔鍥㓶挈 |- | 6:009 || ㄲ || 꼥〮 ˦ 偈碣嵑 |- | 6:009 || ㆁ || ᅌᅨᆼ〯 ˧˦ 掜 ᅌᅨᆼ〮 ˦ ❌甈乂刈㣻艾 |- | 6:010 || ㄷ || 뎽 ˧ 氐互低仾詆呧柢羝牴隄䧑堤鞮䩚磾 뎽〯 ˧˦ 邸䣌柢詆呧阺坻弤抵掋牴觝軝疧疷底㡳氐提堤底 뎽〮 ˦ 帝諦諟𧫚締蔕䗖柢泜氐疐嚔㗣 |- | 6:011 || ㅌ || 톙 ˧ 梯鷈鷉鵜𩀗 톙〯 ˧˦ 體軆躰涕緹紙醍 톙〮 ˦ 替暜朁涕洟鬀鬄剃殢揥楴𧝐褅裼薙雉𡲕傺袲 |- | 6:012 || ㄸ || 똉 ˧ 題提媞偍姼禔褆緹鞮睼瑅騠踶鶗嗁啼諦㖒㖷渧蹏蹄締綈䬾稊𦯔稺䄺𧀾銻𧋘鵜鴺罤䨑荑梯鮧鯷桋鴺㡗折 똉〯 ˧˦ 弟悌娣遞递𮞏 똉〮 ˦ 第弟悌娣睇眱珶遞迭递𮞏遰褅締杕軑釱踶提題鬄髢錫鶗鷤逮棣滯㿃䐭彘 |- | 6:014 || ㄴ || 녱 ˧ 泥埿臡 녱〯 ˧˦ 禰祢檷鑈鈮濔瀰薾爾泥 녱〮 ˦ 泥 |- | 6:015 || ㅂ || 볭 ˧ 篦豍❌㡙鎞❌狴❌ 볭〮 ˦ 閉箄嬖蔽草弊弊鷩鄨廢癈祓苃茀 |- | 6:015 || ㅍ || 폥 ˧ ❌批錍鈚漂 폥〮 ˦ 媲睤❌䁹辟壀埤僻濞淠潎❌肺杮 |- | 6:016 || ㅃ || 뼹 ˧ 鼙鞞𧯿椑㼰膍 뼹〯 ˧˦ 陛陛梐狴陛髀䯗䏶❌脾肶 뼹〮 ˦ 弊獘斃弊幣❌❌薜蘗❌吠犻❌茷 |- | 6:017 || ㅁ || 몡 ˧ 迷麛麑❌❌ 몡〯 ˧˦ 米眯䋛❌洣 몡〮 ˦ 袂 |- | 6:017 || ㅈ || 졩 ˧ 齎䝴韲齏❌虀懠❌擠躋隮齊❌❌ 졩〯 ˧˦ 濟❌㧗 졩〮 ˦ 霽擠濟❌祭際穄制製䱥鰶㫼晣晢淛浙折 |- | 6:018 || ㅊ || 촁 ˧ 妻萋霋凄淒悽緀 촁〯 ˧˦ 泚玼萋緀 촁〮 ˦ 切砌墄❌妻掣❌摯觢挈懘滯❌ |- | 6:018 || ㅉ || 쪵 ˧ 齊臍蠐 쪵〯 ˧˦ 薺齊癠鮆鱭 쪵〮 ˦ 嚌懠穧❌劑齊薺癠齌眥眦 |- | 6:019 || ㅅ || 솅 ˧ 西栖棲犀屖遟凘澌嘶撕 솅〯 ˧˦ 洗灑 솅〮 ˦ 細壻壻棲世貰勢埶殺閷 |- | 6:020 || ㅆ || 쏑 ˧ 栘 쏑〮 ˦ 誓逝遰遞筮❌噬澨澨忕 |- | 6:020 || ㆆ || ᅙᅨᆼ ˧ 鷖繄䃜瑿㙠黳 ᅙᅨᆼ〮 ˦ 瘞❌餲医瞖繄翳蘙嫕瘱曀㙪㙠殪縊 |- | 6:020 || ㅎ || 혱 ˧ 醯❌䤈橀 |- | 6:021 || ㆅ || ᅘᅨᆼ ˧ 兮❌奚㜎傒蹊❌騱貕豀謑鼷嵇 ᅘᅨᆼ〯 ˧˦ 徯諐蹊❌傒謑謑奚 ᅘᅨᆼ〮 ˦ 系繫係結禊妎盻 |- | 6:021 || ㅇ || 옝 ˧ 倪齯鯢蜺䘽輗❌棿麑猊猊霓兒 옝〮 ˦ 曳洩❌袣❌泄呭詍㖂䛖枻栧抴跇裔❌㵝㵩㳿㵩勩詣栺睨倪堄帠羿❌寱藝蓺❌槸褹袂囈 |- | 6:022 || ㄹ || 롕 ˧ 黎❌❌犁黧藜蔾瓈邌梨驪鸝蠡 롕〯 ˧˦ 禮澧醴鱧蠡䗍劙盠欐 롕〮 ˦ 麗䕻儷儷❌欐攦戾唳淚綟棙悷❌蜧茘❌蛠❌❌隷隷劙蠫❌㓯盭沴離例列栵迾裂厲厲礪❌❌糲爄勵蠣❌癘❌㾐❌ |} === ㆋ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:024 || ㄱ || ᄀᆒᆼ ˧ 圭窐❌❌❌閨袿邽蠲 ᄀᆒᆼ〮 ˦ 桂筀䳏 |- | 6:024 || ㅋ || ᄏᆒᆼ ˧ 睽聧奎暌刲㨒 |- | 6:025 || ㄷ || ᄃᆒᆼ〮 ˦ 綴餟醊腏畷錣 |- | 6:025 || ㅈ || ᄌᆒᆼ〮 ˦ 蕝蕞贅 |- | 6:025 || ㅊ || ᄎᆒᆼ〮 ˦ 脃膬毳橇竁喙 |- | 6:025 || ㅅ || ᄉᆒᆼ〮 ˦ 歲繐❌稅帨帥蛻涗裞䬽鐫說 |- | 6:025 || ㅆ || ᄊᆒᆼ〮 ˦ 篲 |- | 6:025 || ㆆ || ᅙᆒᆼ ˧ 烓❌ ᅙᆒᆼ〮 ˦ 穢薉濊獩 |- | 6:026 || ㅎ || ᄒᆒᆼ〮 ˦ 嘒嚖❌暳噦喙𣨶𤸁𠿔顪噦翽 |- | 6:026 || ㆅ || ᅘᆒᆼ ˧ 攜㩗携觿鑴蠵酅❌巂畦 ᅘᆒᆼ〮 ˦ 慧轊惠蕙譓憓 |- | 6:026 || ㅇ || ᄋᆒᆼ〮 ˦ 叡睿銳梲 |- | 6:027 || ㅿ || ᅀᆒᆼ〮 ˦ 汭內枘芮蜹 |} === ㅗ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:027 || ㄱ || 공 ˧ 孤❌觚❌柧軱呱❌箛䉉苽菰罛❌姑酤沽蛄鴣䧸辜❌❌橭❌盬夃 공〯 ˧˦ 古詁故估牯沽酤罟苦楛鼓❌瞽鼓股❌骰羖❌蠱監賈夃 공〮 ˦ 顧雇固凅錮鯝稒㧽痼故酤沽詁蠱 |- | 6:028 || ㅋ || 콩 ˧ 枯刳恗 콩〯 ˧˦ 苦䇢 콩〮 ˦ 庫袴絝胯跨❌ |- | 6:029 || ㆁ || ᅌᅩᆼ ˧ 吾浯梧鼯❌❌郚吳㻍珸鋘鋙 ᅌᅩᆼ〯 ˧˦ 五伍午迕仵旿 ᅌᅩᆼ〮 ˦ 誤悞悟晤旿晤捂梧忤牾午蘁逜寤❌遻❌迕俉愕 |- | 6:029 || ㄷ || 동 ˧ 都闍堵 동〯 ˧˦ 覩堵❌陼暏賭❌楮肚士 동〮 ˦ 妒妬奼㓃宅詫秅秺蠧螙❌斁❌睪 |- | 6:030 || ㅌ || 통 ˧ 稌 통〯 ˧˦ 土吐稌 통〮 ˦ 兎鵵菟吐 |- | 6:031 || ㄸ || 똥 ˧ 徒途墿峹𡷣荼𣘻余涂捈駼鵌鷋䅷䔑塗屠瘏菟𧈋檡兎圖鍍 똥〯 ˧˦ 杜荰土赭𢾅剫 똥〮 ˦ 度渡𣳥鍍塗 |- | 6:031 || ㄴ || 농 ˧ 奴㚢仅孥帑駑笯砮 농〯 ˧˦ 怒弩砮努 농〮 ˦ 笯怒㣽𢘂 |- | 6:032 || ㅂ || 봉 ˧ 逋餔哺誧晡 봉〯 ˧˦ 補䋠❌圃甫❌譜諩 봉〮 ˦ 布尃抪圃 |- | 6:032 || ㅍ || 퐁 ˧ 鋪痡鯆❌❌抪 퐁〯 ˧˦ 普浦誧溥 퐁〮 ˦ 怖鋪誧 |- | 6:033 || ㅃ || 뽕 ˧ 蒲莆蒱酺匍扶䒀瓿 뽕〯 ˧˦ 簿部蔀 뽕〮 ˦ 步❌荹捕哺餔䊇❌酺脯鞴 |- | 6:033 || ㅁ || 몽 ˧ 模橅❌❌謨謩嫫❌❌膜摹摸撫墓母 몽〯 ˧˦ 姥莽茻❌姆 몽〮 ˦ 暮慕募墓謨慔 |- | 6:034 || ㅈ || 종 ˧ 租蒩菹𦼬𧂚𧀽𦵔𦯓苴 종〯 ˧˦ 祖珇駔組阻岨俎柤𤕲齟詛 종〮 ˦ 作詛𥛜謯作 |- | 6:035 || ㅊ || 총 ˧ 麤麆粗觕初 총〯 ˧˦ 楚礎濋憷❌ 총〮 ˦ 措厝錯醋楚 |- | 6:035 || ㅉ || 쫑 ˧ 徂且鉏鋤助耡 쫑〯 ˧˦ 粗麆𧇿 쫑〮 ˦ 祚胙飵阼葄助耡鉏莇 |- | 6:036 || ㅅ || 송 ˧ 蘇穌㢝酥𦣑𨢭蔬疏疎𤕠梳𣐌𣓜疋綀釃斯 송〯 ˧˦ 所𢨷䝪糈 송〮 ˦ 素愫榡傃嗉訴𧪜愬㴑遡溯泝塑塐疏𥿇 |- | 6:037 || ㆆ || ᅙᅩᆼ ˧ 烏於杇圬釫❌汙汚於嗚惡 ᅙᅩᆼ〯 ˧˦ 塢䃖埡瑦鄔 ᅙᅩᆼ〮 ˦ 汙洿盓❌惡䛩䜑堊嗚 |- | 6:037 || ㅎ || 홍 ˧ 呼虖謼嘑滹❌淲❌❌泘膴幠芋 홍〯 ˧˦ 虎琥滸滹許 홍〮 ˦ 謼嘑呼戽 |- | 6:038 || ㆅ || ᅘᅩᆼ ˧ 胡❌𠴱箶❌湖瑚餬❌䭌醐❌糊粘❌❌❌䉿❌鶘乎虖壷弧狐❌瓠葫 ᅘᅩᆼ〯 ˧˦ 戶昈雇❌扈❌怙❌岵祜酤楛鄠❌下芐芦嫭嫮橭 ᅘᅩᆼ〮 ˦ 護濩頀䪝穫互枑冱瓠嫮嫭涸 |- | 6:039 || ㄹ || 롱 ˧ 盧蘆籚廬鑢爐櫨鱸壚艫轤瓐黸獹瀘矑纑顱髗❌❌玈❌❌ 롱〯 ˧˦ 魯櫓櫓㯭艣虜擄鹵滷塷瀂❌ 롱〮 ˦ 路璐露簬❌鷺❌鸕鴼潞輅賂 |} === ㅏ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:041 || ㄱ || 강 ˧ 歌謌❌戨❌牁哥柯鴚舸渮荷渮嘉佳加笳茄鴐駕珈耞跏迦痂枷葭麚豭猳家 강〯 ˧˦ 哿笴舸菏荷❌賈檟榎夏假徦嘏椵斝斝 강〮 ˦ 箇個介駕架榢枷稼嫁幏假叚下斝斝價賈 |- | 6:042 || ㅋ || 캉 ˧ 珂砢軻呿 캉〯 ˧˦ 可岢軻坷 캉〮 ˦ 軻坷髂䯊❌ |- | 6:043 || ㆁ || ᅌᅡᆼ ˧ 莪峨峨我娥俄哦睋蛾䖸鵝❌䳘鵞牙芽枒㧎衙涯厓 ᅌᅡᆼ〯 ˧˦ 我騀硪峨雅鴉庌牙疋 ᅌᅡᆼ〮 ˦ 餓訝迓御輅衙庌砑牙 |- | 6:044 || ㄷ || 당 ˧ 多多奓 당〯 ˧˦ 嚲癉哆打 당〮 ˦ 癉憚咤喥䖳㓃宅❌奼哆奓 |- | 6:044 || ㅌ || 탕 ˧ 佗他拕拖扡蛇詑訑 탕〯 ˧˦ 姹 탕〮 ˦ 詫❌咤侘差 |- | 6:045 || ㄸ || 땅 ˧ 駝駞它沱沲池陀岮陁阤跎❌佗紽酡鮀迤池鼉鱓鱣驒馱秅茶𣘻涂塗 땅〯 ˧˦ 拕拖扡佗柁舵䑨杕袉沱❌❌沲阤陀陊爹 땅〮 ˦ 馱他大 |- | 6:046 || ㄴ || 낭 ˧ 那難單儺拏挐笯 낭〯 ˧˦ 娜那袲橠難❌旎儺 낭〮 ˦ 柰那哪㖠 |- | 6:047 || ㅂ || 방 ˧ 波碆番僠嶓巴笆芭豝❌羓鈀 방〯 ˧˦ 跛❌簸播把 방〮 ˦ 播譒磻簸霸伯灞壩䃻靶弝杷欛 |- | 6:048 || ㅍ || 팡 ˧ 頗❌坡岥❌陂❌玻葩苩舥芭 팡〯 ˧˦ 頗叵 팡〮 ˦ 破帊帕袙怕❌❌ |- | 6:048 || ㅃ || 빵 ˧ 婆鄱番皤❌番杷爬把琶 빵〮 ˦ 縛杷䆉❌罷 |- | 6:049 || ㅁ || 망 ˧ 摩擵磨❌麼❌䯢魔劘攠麻菻蟆䗫蟇 망〯 ˧˦ 麼䯢馬 망〮 ˦ 磨禡貊伯榪罵䣕䣖鬕 |- | 6:049 || ㅈ || 장 ˧ 樝❌𣕈柤齟 장〯 ˧˦ 左𡯛鮓䱹𩽫苴苲 장〮 ˦ 左佐𡯛作詐醡笮苲榨𨣮𨢦溠咋 |- | 6:050 || ㅊ || 창 ˧ 蹉嵯磋瑳搓差叉杈靫扠差艖舣鎈 창〯 ˧˦ 瑳 창〮 ˦ 磋蹉差汊衩 |- | 6:051 || ㅉ || 짱 ˧ 醝鹺艖𦪸䑘㽨瘥𣩈嵳𥰭齹䣜鄼 |- | 6:051 || ㅅ || 상 ˧ 娑挱莎莏挲沙䤬𨪍桫髿𣯌犠獻戲傞𠈱沙𣲡砂紗𦀟鯊㲚髿硰 상〯 ˧˦ 娑灑洒葰傻 상〮 ˦ 些娑逤嗄𣣺沙 |- | 6:052 || ㅆ || 쌍 ˧ 槎楂苴 쌍〯 ˧˦ 槎𠞊柞 쌍〮 ˦ 乍䄍蓌 |- | 6:053 || ㆆ || ᅙᅡᆼ ˧ 阿娿痾䋪䋍鴉鴉丫啞亞 ᅙᅡᆼ〯 ˧˦ 娿婀妸❌阿猗啞瘂❌ ᅙᅡᆼ〮 ˦ 亞惡啞稏婭 |- | 6:053 || ㅎ || 항 ˧ 訶苛何呵㰤呀谺岈❌鰕 항〯 ˧˦ 㰤閜 항〮 ˦ 呵罅㙤釁❌❌嚇赫哧謑 |- | 6:054 || ㆅ || ᅘᅡᆼ ˧ 何荷河苛遐徦假蕸霞瑕鍜碬蝦鰕騢假赮 ᅘᅡᆼ〯 ˧˦ 荷何❌抲苛下夏廈 ᅘᅡᆼ〮 ˦ 賀❌荷何暇假嘉下芐夏 |- | 6:055 || ㄹ || 랑 ˧ 羅蘿籮❌鑼欏囉饠 랑〯 ˧˦ 砢❌㦬❌攞邏 |} === ㅑ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:055 || ㄱ || 걍 ˧ 迦 |- | 6:055 || ㅋ || 컁 ˧ 呿 |- | 6:055 || ㄲ || 꺙 ˧ 伽茄 |- | 6:055 || ㅈ || 쟝 ˧ 嗟罝𦋽遮庶 쟝〯 ˧˦ 姐她媎者赭堵 쟝〮 ˦ 借藉唶柘䂞蔗𤯈𤯋鷓蟅䗪炙 |- | 6:056 || ㅊ || 챵 ˧ 車 챵〯 ˧˦ 且奲撦䰩奓哆拸䞣 |- | 6:056 || ㅉ || 쨩〯 ˧˦ 抯飷 쨩〮 ˦ 藉蒩耤 |- | 6:057 || ㅅ || 샹 ˧ 些𡭟奢賖畬 샹〯 ˧˦ 寫𣞐瀉捨舍 샹〮 ˦ 卸瀉舍赦厙 |- | 6:057 || ㅆ || 썅 ˧ 邪耶斜䔑𦳃闍堵蛇虵鉈 썅〯 ˧˦ 䄬社 썅〮 ˦ 謝榭㴬射䠶麝貰 |- | 6:058 || ㅇ || 양 ˧ 邪釾鋣鎁枒椰椰耶爺斜 양〯 ˧˦ 野野也冶蠱 양〮 ˦ 夜射 |- | 6:058 || ㅿ || ᅀᅣᆼ〯 ˧˦ 惹❌若 |} === ㅘ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:058 || ㄱ || 광 ˧ 戈過撾渦❌鍋鈛❌瓜媧騧❌蝸緺 광〯 ˧˦ 果菓惈❌裹蜾裸寡❌另剮 광〮 ˦ 過裹燴 |- | 6:059 || ㅋ || 쾅 ˧ 科蝌窠薖簻誇侉胯跨姱恗夸❌荂華 쾅〯 ˧˦ 顆堁果骻胯跨㡁袴銙❌錁❌夸 쾅〮 ˦ 課堁跨❌夸胯袴❌ |- | 6:060 || ㄲ || 꽝 ˧ 瘸 |- | 6:060 || ㆁ || ᅌᅪᆼ ˧ 吪❌囮繇鈋譌訛 ᅌᅪᆼ〯 ˧˦ 瓦 ᅌᅪᆼ〮 ˦ 臥 |- | 6:060 || ㄷ || 돵 ˧ 檛薖撾 돵〯 ˧˦ 朶揣挅捶㪜崜埵𥠄鬌 |- | 6:061 || ㅌ || 퇑 ˧ 詑 퇑〯 ˧˦ 妥綏𢼻隋橢䲊墮媠 퇑〮 ˦ 唾涶佗扡拕拖大 |- | 6:061 || ㄸ || 뙁〯 ˧˦ 惰嶞墮墯隓𡐦垜𨹃䅜 뙁〮 ˦ 惰憜媠墮𢞑𢢠 |- | 6:062 || ㄴ || 놩 ˧ 捼挼 놩〮 ˦ 愞懦偄𦓏稬糯𤲬堧壖 |- | 6:062 || ㅈ || 좡 ˧ 髽 좡〮 ˦ 挫蓌夎䟶 |- | 6:062 || ㅊ || 촹〯 ˧˦ 脞 촹〮 ˦ 剉挫莝摧 |- | 6:062 || ㅉ || 쫭 ˧ 矬痤 쫭〯 ˧˦ 坐 쫭〮 ˦ 坐座 |- | 6:063 || ㅅ || 솽 ˧ 蓑莎梭𥭟唆 솽〯 ˧˦ 鎖鏁瑣璅䵀葰 |- | 6:063 || ㆆ || ᅙᅪᆼ ˧ 倭踒渦窩窊窳溛窪窐洼❌哇娃蛙䵷汙 ᅙᅪᆼ〯 ˧˦ 婐果 ᅙᅪᆼ〮 ˦ 涴汙堁 |- | 6:064 || ㅎ || 황 ˧ 鞾靴❌❌㞜花華❌ 황〯 ˧˦ 火 황〮 ˦ 貨化 |- | 6:064 || ㆅ || ᅘᅪᆼ ˧ 和龢鉌禾華崋譁嘩驊❌鋘釫鏵 ᅘᅪᆼ〯 ˧˦ 禍❌輠❌夥❌髁踝裸裸觟 ᅘᅪᆼ〮 ˦ 和華樺檴嬅摦擭獲鱯㕦 |- | 6:065 || ㄹ || 뢍 ˧ 鸁䯁騾臝蠃螺蠡蝸❌❌鏍虆蔂❌❌覼 뢍〯 ˧˦ 裸裸果臝儽蠃蓏❌蠡❌瘰攭 뢍〮 ˦ 邏摞 |} === ㅜ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:066 || ㄱ || 궁 ˧ 拘佝句❌跔駒驕痀俱㪺❌仇捄 궁〯 ˧˦ 矩榘距句拒枸蒟萬楀踽偊鄅 궁〮 ˦ 屨鞻句絇蒟瞿懼 |- | 6:067 || ㅋ || 쿵 ˧ 區驅毆敺❌歐嶇軀❌鮬摳 쿵〯 ˧˦ 齲踽 쿵〮 ˦ 驅 |- | 6:067 || ㄲ || 꿍 ˧ 劬朐❌絇句❌❌䋧軥枸鼩翑句臞癯躣躩忂灈懼戵鑺欋衢瞿鸜 꿍〯 ˧˦ 寠窶 꿍〮 ˦ 懼瞿具❌❌ |- | 6:068 || ㆁ || ᅌᅮᆼ ˧ 虞❌澞鸆麌娛禺愚隅嵎髃腢喁齵堣鍝于竽𥫡迂盂❌杅❌玗釪汙邘❌雩❌謣❌ ᅌᅮᆼ〯 ˧˦ 麌❌俁㒁羽禹䨞萭瑀偊宇㝢㡰❌雨 ᅌᅮᆼ〮 ˦ 遇寓庽禺虞芌芋𩁹吁羽雨 |- | 6:070 || ㅂ || 붕 ˧ 膚❌夫玞扶䄮鈇柎不❌枎跗趺附❌枹 붕〯 ˧˦ 甫父莆黼❌脯俌簠❌府腑俯斧鈇 붕〮 ˦ 付傅❌賦富 |- | 6:071 || ㅍ || 풍 ˧ 敷傳鋪尃溥懯孚莩罦䍖❌稃粰❌❌桴俘郛垺𡎽苻荴紨泭柎❌怤荂❌旉華麩麱䴸鄜 풍〯 ˧˦ 撫❌拊柎弣 풍〮 ˦ 赴訃仆踣掊䞳趏簠副報 |- | 6:072 || ㅃ || 뿡 ˧ 扶蚨颫夫❌芙符苻❌鳧瓿 뿡〯 ˧˦ 父㕮駁輔䩉❌鬴釜❌秿❌滏腐焤 뿡〮 ˦ 附胕柎付駙䮛鮒❌柎坿跗傅賻負負萯偩婦 |- | 6:073 || ㅁ || 뭉 ˧ 無亡蕪䍢❌璑毋巫誣 뭉〯 ˧˦ 武鵡母碔珷璑舞儛廡❌膴嫵斌憮㒇嘸甒❌❌橆蕪侮侮務䍙堥 뭉〮 ˦ 務婺瞀騖鶩霧 |- | 6:074 || ㅊ || 충 ˧ 芻䅳 |- | 6:074 || ㅉ || 쭝 ˧ 雛䅳媰 |- | 6:074 || ㅅ || 숭 ˧ 毹毺㲣𡨙氀 숭〮 ˦ 數捒 |- | 6:074 || ㆆ || ᅙᅮᆼ ˧ 紆汙迂盓陓 ᅙᅮᆼ〯 ˧˦ 傴痀❌嫗噢 ᅙᅮᆼ〮 ˦ 嫗饇歐燠噢 |- | 6:075 || ㅎ || 훙 ˧ 訏吁盱❌芋❌旴晇冔❌昫煦蓲姁喣嘔呴謳 훙〯 ˧˦ 詡栩珝訏冔❌姁昫喣煦煦膴咻 훙〮 ˦ 煦昫呴欨咻休䣱酗 |- | 6:076 || ㄹ || 룽 ˧ 慺膢褸漊鏤氀❌蔞𦭯婁 룽〯 ˧˦ 縷僂軁漊嶁㟺褸簍蔞婁 룽〮 ˦ 屢婁僂 |} === ㅠ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:077 || ㄷ || 듕 ˧ 株誅蛛跦邾禂 듕〯 ˧˦ 𪐴拄柱 듕〮 ˦ 駐軴柱住𨙦𩦛 |- | 6:077 || ㅌ || 튱 ˧ 貙 |- | 6:077 || ㄸ || 뜡 ˧ 廚躕裯幮幬趎 뜡〯 ˧˦ 柱 뜡〮 ˦ 住 |- | 6:078 || ㅈ || 즁 ˧ 諏𧩻娵朱珠侏咮祩袾 즁〯 ˧˦ 主宔砫袾麈炷枓斗 즁〮 ˦ 足注屬主註炷鉒䪒澍霔咮𠰍鑄馵 |- | 6:078 || ㅊ || 츙 ˧ 趨𨃘趣騶取樞姝 츙〯 ˧˦ 取 츙〮 ˦ 娶趣趨 |- | 6:079 || ㅉ || 쯍〯 ˧˦ 聚 쯍〮 ˦ 聚鄹𡒍㝡 |- | 6:079 || ㅅ || 슝 ˧ 須𩓣𥪙𥪥䇕𡡓鬚需繻緰㡏𦅎𪋯輸隃鄃 슝〯 ˧˦ 數籔簍藪 슝〮 ˦ 戍隃輸束 |- | 6:080 || ㅆ || 쓩 ˧ 殊銖洙㼡茱殳杸 쓩〯 ˧˦ 豎𠐊侸裋𧞫樹 쓩〮 ˦ 樹澍裋𧞫 |- | 6:080 || ㅇ || 융 ˧ 兪逾踰𧼯窬瘉瘐𨵦渝楡揄𦩞羭蝓𧍳堬瑜牏褕喩愉婾愈睮覦歈㼶臾㬰諛楰腴㥚萸 융〯 ˧˦ 庾㢏㔱楰㥚斞斔愉瘐瘉臾愈瘉癒貐窳寙 융〮 ˦ 裕䘱欲慾籲諭喩覦兪瘉 |- | 6:082 || ㅿ || ᅀᅲᆼ ˧ 儒𠍶嚅吺咮懦愞濡𣽉醹襦𧝄 ᅀᅲᆼ〯 ˧˦ 乳醹㳶 ᅀᅲᆼ〮 ˦ 孺𡦗乳 |} === ㅓ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:083 || ㄱ || 겅 ˧ 居㞐琚❌裾据椐鶋❌車 겅〯 ˧˦ 擧莒筥❌籧❌柜欅弆 겅〮 ˦ 據鐻踞倨裾鋸據居擧 |- | 6:083 || ㅋ || 컹 ˧ 墟丘嶇㠊袪祛佉胠阹呿抾魼鱋去 컹〯 ˧˦ 去麮胠 컹〮 ˦ 去㰦呿胠 |- | 6:084 || ㄲ || 껑 ˧ 渠蕖❌❌❌磲❌蘧遽籧鐻❌璩璖❌醵䣰腒 껑〯 ˧˦ 巨鉅詎渠距❌❌拒歫蚷駏❌炬❌秬岠怇苣虡簴❌❌鐻 껑〮 ˦ 遽蘧懅醵䣰勮詎巨渠 |- | 6:085 || ㆁ || ᅌᅥᆼ ˧ 魚䁩❌䐳❌漁䱷䰻齬衙 ᅌᅥᆼ〯 ˧˦ 語齬鋙❌峿敔梧衙圄圉禦御蘌❌❌❌ ᅌᅥᆼ〮 ˦ 御御禦衙圉語䱷漁 |- | 6:086 || ㆆ || ᅙᅥᆼ ˧ 於淤唹 ᅙᅥᆼ〮 ˦ 飫饇❌❌棜淤❌瘀菸 |- | 6:086 || ㅎ || 헝 ˧ 虛虗歔噓吁喣呴驉❌魖 헝〯 ˧˦ 許 헝〮 ˦ 噓 |} === ㅕ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:087 || ㄷ || 뎡 ˧ 豬猪䐗櫫瀦潴 뎡〯 ˧˦ 貯𡪄著紵䘢宁 뎡〮 ˦ 著 |- | 6:087 || ㅌ || 텽 ˧ 攄捈樗櫖摴摢㻬 텽〯 ˧˦ 楮柠褚 텽〮 ˦ 絮 |- | 6:088 || ㄸ || 뗭 ˧ 除篨滁筡著躇屠儲宁 뗭〯 ˧˦ 宁佇竚紵𦂂羜泞眝坾杼拧䇡抒苧 뗭〮 ˦ 箸櫡躇著宁除 |- | 6:088 || ㄴ || 녕 ˧ 袽帤挐拏 녕〯 ˧˦ 女 녕〮 ˦ 女 |- | 6:089 || ㅈ || 졍 ˧ 苴蒩且蛆沮諸䃴蠩 졍〯 ˧˦ 苴疽䰞𩱰𤑨煮渚陼庶 졍〮 ˦ 怚姐𢚆沮苴翥䬡庶 |- | 6:089 || ㅊ || 쳥 ˧ 疽砠❌沮趄跙狙雎苴 쳥〯 ˧˦ 且杵処處 쳥〮 ˦ 覰䁦狙蜡處 |- | 6:090 || ㅉ || 쪙〯 ˧˦ 沮咀跙 |- | 6:090 || ㅅ || 셩 ˧ 胥諝㥠胥糈稰蝑湑書𦘠舒豫𪅰紓䋡悆忬瑹荼 셩〯 ˧˦ 諝醑湑稰胥暑黍鼠癙 셩〮 ˦ 絮恕庶 |- | 6:091 || ㅆ || 쎵 ˧ 徐邪蜍蠩 쎵〯 ˧˦ 敍漵㵰序䦽㘧茅𣏗杼藇𨣦鱮魣嶼緖紓墅野杼茅𣏗桃 쎵〮 ˦ 署𥌓暏 |- | 6:092 || ㅇ || 영 ˧ 余予畬❌畭蜍餘與歟欤懙❌㦛旟璵㼂譽鸒輿❌舁伃妤茅雓 영〯 ˧˦ 與❌歟予❌ 영〮 ˦ 豫舒余與鸒譽礜藇蕷穥歟輿預忬澦❌悆 |- | 6:093 || ㄹ || 령 ˧ 臚❌盧膚驢䮫廬慮藘閭櫚 령〯 ˧˦ 呂呂侶梠膂旅❌祣❌臚穭稆儢❌ 령〮 ˦ 慮爈❌䥨鋁❌濾勴錄窶 |- | 6:094 || ㅿ || ᅀᅧᆼ ˧ 如鴐茹絮洳 ᅀᅧᆼ〯 ˧˦ 汝籹❌女茹 ᅀᅧᆼ〮 ˦ 茹洳如 |} {{단원 안내|책=동국정운 색인|상위=동국정운 색인 |이전=끝소리 ㅱ |다음=끝소리 ㆁ}} [[분류:동국정운]] rz7xq6u6cvwceeemi8uyr7ai63nvzvp 45125 45124 2024-10-30T18:45:44Z 2600:1700:C76:9010:22A7:9C6B:AF28:E81F /* ㅐ */ 45125 wikitext text/x-wiki === ㆍ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:008 || ㅈ || ᄌᆞᆼ ˧ 貲訾𤺒訿觜髭𩑽鄑咨諮資䆅姿粢秶澬姕𪗋齊𧞓齍璾齎玆茲孳滋嵫鎡仔孜耔鼒 ᄌᆞᆼ〯 ˧˦ 紫訿𤺒啙呰訾跐㧗姊秭子耔芓仔杍梓榟 ᄌᆞᆼ〮 ˦ 恣積𥡯㧘柴 |- | 5:009 || ㅊ || ᄎᆞᆼ ˧ 雌䳄郪趑次𨀥𨒮❌ ᄎᆞᆼ〯 ˧˦ 此泚佌𠈈𢇌玼皉 ᄎᆞᆼ〮 ˦ 刺𧧒庛❌𢉪次佽㐸絘䯸 |- | 5:010 || ㅉ || ᄍᆞᆼ ˧ 慈㤵磁鶿鷀鴜茲茨餈𩜴𥻓粢䭣薋𩆂䨏𩆃瓷❌薺疵玼骴髊𩨱茈𦶉訾 ᄍᆞᆼ〮 ˦ 字牸孶自漬眥眦骴髊㱴餗 |- | 5:011 || ㅅ || ᄉᆞᆼ ˧ 私思罳䰄偲愢緦颸絲司伺覗𥄶斯撕澌凘𪆁廝㒋虒傂禠㴲磃𩆵菥析徙師獅𤜳篩釃𦌿灑廝襹褷籭簛簁蓰 ᄉᆞᆼ〯 ˧˦ 徙璽死枲葸𤟧諰鰓禗躧蹝屣釃纚縰灑洒矖𥊂𩌦𩎉蓰簁籭漇史駛使 ᄉᆞᆼ〮 ˦ 賜錫瀃澌𣩠杫𣘩㮐四泗柶駟肆𩬶肄笥伺覗司僿思𩢲駛使𩌦屣灑洒曬釃䚕 |- | 5:013 || ㅆ || ᄊᆞᆼ ˧ 詞祠柌辭辤漦 ᄊᆞᆼ〯 ˧˦ 兕𧰽𠒃似仏姒㚶耜梩𦓨杞䎣枱𨐠祀𥘰𧝀汜巳士仕𣐈戺俟竢𥏳𢓪䇃𢈟𢉡逘𣀼𡱢涘騃 ᄊᆞᆼ〮 ˦ 寺嗣飤飼食飴事 |} === ㅣ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:014 || ㅋ || 킹〯 ˧˦ 企跂𠈮 킹〮 ˦ 棄企❌跂蚑 |- | 5:015 || ㄲ || 낑 ˧ 祇軝❌軙忯疧疷伎跂枝蚑𨙸岐歧祁耆鰭愭鬐 |- | 5:015 || ㄷ || 딩 ˧ 知𥎿䝷蜘𧐉鼅胝疷䟡躓𦙁 딩〯 ˧˦ 徵黹𧝉䌤緻絺希鵗 딩〮 ˦ 致致輊輖𦥎䡹質劕躓懫懥驇疐𨇈嚔❌𢷟智知置 |- | 5:016 || ㅌ || 팅 ˧ 摛攡螭彲離𡖟魑离𣉽絺瓻癡𢣕𠈴笞抬𪗪呞 팅〯 ˧˦ 褫搋拸䚦扡扯恥誀祉 팅〮 ˦ 眙 |- | 5:017 || ㄸ || 띵 ˧ 馳䮈池篪竾筂褫簃𠗺謻趨邸踟踶墀𡎰遲遟𨒈赿❌䜄坻汣𡊆沶泜蚳𧐏貾治耛菭持蛇 띵〯 ˧˦ 豸𧋈褫傂廌阤陁陊杝雉鴙鶨薙埃垁滍泜踶峙𠍰跱畤庤𤲵痔偫崻 띵〮 ˦ 地坔埊緻致穉稚䆈䄺䕌謘遲遟治値直植置褫彘 |- | 5:019 || ㄴ || 닝 ˧ 尼㞾怩跜秜旎 닝〯 ˧˦ 柅旎狔 닝〮 ˦ 膩𦡸 |- | 5:020 || ㅂ || 빙 ˧ 卑庳箄裨陴埤鞞錍椑陂波詖羆❌❌罷藣❌❌碑悲非餥裴蜚誹緋扉飛騛 빙〯 ˧˦ 俾卑髀脾鞞箄匕比妣䃾❌秕粃❌朼枇彼佊鄙啚否匪篚榧棐蜚奜蜰 빙〮 ˦ 臂嬖辟畀❌痹比庛芘賁跛詖陂披佊貱波藣祕秘泌柲鉍䪐閟毖鄪費❌粊轡䩛沸髴❌誹非芾茀 |- | 5:022 || ㅍ || 핑 ˧ 紕❌❌悂鈹鉟披㱟㨢被旇翍耚❌丕㔻伾秠怌駓䮆豾狉銔批霏䬠菲騑匪❌馡裶裴妃婓❌ 핑〯 ˧˦ 庀比庇疕仳諀吡批披㱟噽秠❌斐菲誹非悱朏 핑〮 ˦ 譬辟濞淠帔被襬費 |- | 5:024 || ㅃ || 삥 ˧ 毗紕辟𨈚❌❌阰❌魮❌鈚蚍螕琵批比膍肶㮰貔豼❌❌陴埤脾裨崥皮疲罷郫邳岯坯❌魾肥淝腓痱❌厞婓賁 삥〯 ˧˦ 婢埤䠋卑庳埤否痞岯圮䤏被骳罷陫❌ 삥〮 ˦ 鼻比紕枇𢈷庳卑避辟髲被鞁骳備俻犕糒奰贔屝厞陫❌痱腓菲蜚翡䠊剕❌費狒𥜿𥝃𦦻𤲳昲𪎰黂䆊 |- | 5:027 || ㅁ || 밍 ˧ 彌弥𢏏㜷𡝠瀰冞𥹐糜𩞁𩞇𪎭穈𪐎縻𥿫𦄐䌕靡㸏𤓒𦗕劘𪎕𠞧醾𨣿醿蘼蘪攠❌眉嵋湄𣽪𤃰攗楣郿媚麋麛黴微𧗬薇㵟溦霺 밍〯 ˧˦ 弭渳彌瀰𣴱沔敉侎𢘺芉美渼媺靡𦗕尾亹斖娓 밍〮 ˦ 寐媢媚郿篃魅靡未味 |- | 5:028 || ㅈ || 징 ˧ 支枝肢𨈪𨈙跂䧴鳷巵觶𧣨梔氏祇祗衹褆㩼多秖榰禔疧脂祗砥厎泜之芝 징〯 ˧˦ 紙帋坁汦汣❌抵觗㧗抵砥只𠮡軹枳咫𦐖❌疻旨𣅀指𢫾脂恉厎底𦒿止芷茝沚洔阯址趾畤 징〮 ˦ 寘忮伎觶𧣨觗至摯贄質鷙𪁊礩志誌識痣䏯織綕𥿮幟𢂴笫 |- | 5:031 || ㅊ || 칭 ˧ 蚩嗤𢾫媸鴟鵄眵 칭〯 ˧˦ 侈奓鉹𨪐誃哆恀䏧姼𡚼㶴袳移𧛧袲㢋㢁懘齒茝䊼 칭〮 ˦ 熾𧹹幟織旘𢂴志饎糦喜𩛉𩜮𩜂埴𡑌戠 |- | 5:032 || ㅅ || 싱 ˧ 施葹鍦鉇𥍸𥍢絁䌤𦂛䙾𧠜𧠉翅尸㕧鳲𨾋䲩屍蓍詩邿 싱〯 ˧˦ 弛𢐋㢮阤施豕矢笑屎始 싱〮 ˦ 翅𦐊翄翨𦑧施鍦釶啻翅試式弑殺煞𢎍識幟織始失 |- | 5:033 || ㅆ || 씽 ˧ 茬茌匙㮛堤提𦑡禔媞鍉時塒鰣 씽〯 ˧˦ 是諟惿媞𨸝䟗氏視市恃㤄𦧇舐𦧧咶狧 씽〮 ˦ 示謚𧨦豉嗜耆𩝙𨢍𦞯呩視侍寺䦙蒔 |- | 5:034 || ㆆ || ᅙᅵᆼ ˧ 伊洢咿吚黟黝❌ ᅙᅵᆼ〮 ˦ 縊 |- | 5:035 || ㅎ || 힝 ˧ 㕧屎䐖❌❌❌ |- | 5:035 || ㅇ || 잉 ˧ 移拖❌䔟簃❌扅❌栘鉹袲椸箷䈕㮛❌暆貤酏匜詑訑施❌❌蛇夷尼❌痍荑恞𢓡跠峓鐵侇桋䧅姨洟❌胰寅夤彛飴❌❌❌䬮怡眙詒❌貽瓵怠台頤宧圯異 잉〯 ˧˦ 以已苢苡酏祂肔胣匜迆迤施崺❌ 잉〮 ˦ 易㑥敡施袘❌緆衪貤❌移肄肆勩❌廙異異食詒貽 |- | 5:037 || ㄹ || 링 ˧ 離蘺籬杝㰚㒿䍦❌灕漓離攡摛褵縭璃䅻醨离鸝鵹❌❌❌矖纚驪孋穲䕻❌麗罹蠡盠❌劙❌棃❌棃梨犂黧❌犂❌❌犁蔾❌菞釐剺❌氂犛狸剺嫠來倈徠䅘貍狸❌猍梩❌ 링〯 ˧˦ 里理鯉❌俚悝裏李邐麗履 링〮 ˦ 詈茘離离利莅蒞位吏 |- | 5:040 || ㅿ || ᅀᅵᆼ ˧ 兒唲而胹臑腝❌❌洏聏陑栭鮞髵耏耐鴯怠❌‗ 濡 ᅀᅵᆼ〯 ˧˦ 爾爾爾邇邇邇耳耳珥駬䋙餌柅鑈檷 ᅀᅵᆼ〮 ˦ 二貳㒃樲餌㢽❌珥鉺衈咡刵毦髶聏 |} === ㆎ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:041 || ㄷ || ᄃᆡᆼ〮 ˦ 戴載襶 |- | 5:041 || ㅌ || ᄐᆡᆼ ˧ 胎孡台能駘鮐邰斄釐漦 ᄐᆡᆼ〮 ˦ 貸態詒紿怠䈚箈殆待 |- | 5:041 || ㄸ || ᄄᆡᆼ ˧ 臺𡌬薹籉儓擡鮐駘跆苔炱炲䈚箈 ᄄᆡᆼ〯 ˧˦ 待𥩳逮迨隶𨽿駘紿詒殆怠䈚箈靆 ᄄᆡᆼ〮 ˦ 代岱袋黛逮曃靆棣埭𥓏𡍖瑇❌毒玳 |- | 5:043 || ㅂ || ᄇᆡᆼ ˧ 杯盃𦈶𦈧𠤯桮 ᄇᆡᆼ〮 ˦ 背輩軰 |- | 5:043 || ㅍ || ᄑᆡᆼ ˧ 胚妚衃阫坏培醅❌ ᄑᆡᆼ〯 ˧˦ 朏 ᄑᆡᆼ〮 ˦ 配妃朏 |- | 5:043 || ㅃ || ᄈᆡᆼ ˧ 裴徘俳培陪倍阫 ᄈᆡᆼ〯 ˧˦ 琲㻗痱❌倍 ᄈᆡᆼ〮 ˦ 佩佩背偝負倍㔨北邶鄁倍焙❌孛茀勃誖悖哱怫㫲琲㻗拔萯 |- | 5:044 || ㅁ || ᄆᆡᆼ ˧ 枚玫梅楳某槑❌鋂脢脄❌膴酶䊈莓每䍙禖媒煤塺 ᄆᆡᆼ〯 ˧˦ 浼每痗脢脄 ᄆᆡᆼ〮 ˦ 妹昧㫚眛沬韎❌每痗脢脄沕琩❌冒媒 |- | 5:046 || ㅈ || ᄌᆡᆼ ˧ 哉裁栽𦳦載災灾菑甾𤆄 ᄌᆡᆼ〯 ˧˦ 宰䏁縡載 ᄌᆡᆼ〮 ˦ 再載縡 |- | 5:046 || ㅊ || ᄎᆡᆼ ˧ 猜偲 ᄎᆡᆼ〯 ˧˦ 采採寀彩棌綵䰂茝 ᄎᆡᆼ〮 ˦ 菜寀采縩 |- | 5:046 || ㅉ || ᄍᆡᆼ ˧ 裁才材財鼒纔 ᄍᆡᆼ〯 ˧˦ 在 ᄍᆡᆼ〮 ˦ 在裁栽載酨 |- | 5:047 || ㅅ || ᄉᆡᆼ ˧ 鰓䚡䰄思罳❌顋毸 ᄉᆡᆼ〮 ˦ 塞僿簺賽 |- | 5:047 || ㆆ || ᅙᆡᆼ ˧ 哀埃㶼欸唉 ᅙᆡᆼ〯 ˧˦ 欸唉款靉靄藹娭毐 ᅙᆡᆼ〮 ˦ 愛靉曖僾薆藹靄欸 |- | 5:048 || ㅎ || ᄒᆡᆼ ˧ 咍 ᄒᆡᆼ〯 ˧˦ 海❌醢❌ |- | 5:048 || ㆅ || ᅘᆡᆼ ˧ 孩㜾❌頦 ᅘᆡᆼ〯 ˧˦ 亥侅劾 ᅘᆡᆼ〮 ˦ 瀣劾 |- | 5:048 || ㄹ || ᄅᆡᆼ ˧ 來逨❌倈萊徠䅘❌騋淶崍 ᄅᆡᆼ〮 ˦ 徠倈來睞賚萊 |} === ㅢ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:049 || ㄱ || 긩 ˧ 羈䩭鞿羇踦奇倚踦奇畸觭掎剞飢饑❌肌❌姬箕❌❌箕諆❌錤棋檱踑其基朞期祺居機鐖璣禨嘰饑鞿磯譏耭蟣幾刏 긩〯 ˧˦ 己紀几機㲹麂掎踦剞❌庋攱庪㨳蟣❌穖幾 긩〮 ˦ 寄徛❌冀兾驥❌懻❌穊❌穖覬幾洎記其忌已㤅近旣漑曁 |- | 5:051 || ㅋ || 킝 ˧ 敧崎觭踦䗁徛碕榿欺倛僛❌娸❌❌❌ 킝〯 ˧˦ 綺觭起杞梩❌㞯玘芑豈䔇 킝〮 ˦ 器❌器亟氣❌乞❌ |- | 5:052 || ㄲ || 끵 ˧ 奇騎錡琦碕埼隑其萁稘期琪淇祺麒騏鶀❌旗棊碁櫀蜝綦綨❌❌𤪌璂蘄祈❌蘄肵旂頎畿圻幾❌刏❌機碕 끵〯 ˧˦ 技伎妓䗁錡跽❌ 끵〮 ˦ 芰茤騎䗁妓技❌洎垍曁❌墍蔇忌鵋誋惎㥍諅綦禨璣幾曁刏 |- | 5:054 || ㆁ || ᅌᅴᆼ ˧ 宜義儀❌檥轙議鸃䴊㕒嶬涯崖疑儗嶷觺沂凒皚溰 ᅌᅴᆼ〯 ˧˦ 蟻蟻蛾❌❌轙艤檥礒硪齮錡擬譺懝儗疑薿❌孴❌矣顗 ᅌᅴᆼ〮 ˦ 義誼❌竩議儗❌毅藙 |- | 5:055 || ㅈ || 즹 ˧ 菑載𤰭䅔𦿨淄甾緇純䊷輜椔錙鶅𨿴甾 즹〯 ˧˦ 滓胏𦞤𦙰笫緇 즹〮 ˦ 胾椔緇菑倳事輜剚𨧫 |- | 5:056 || ㅊ || 칑 ˧ 差嵯嵳䡨𨍃 칑〮 ˦ 廁厠 |- | 5:056 || ㆆ || ᅙᅴᆼ ˧ 漪猗䝝兮椅欹旖禕醫毉噫意億懿嘻譆❌衣依譩 ᅙᅴᆼ〯 ˧˦ 倚奇椅檹輢猗旖㫊䭲譩醫醷臆旖㕈庡依偯 ᅙᅴᆼ〮 ˦ 意薏倚猗輢懿饐❌饖撎衣䵝 |- | 5:058 || ㅎ || 힁 ˧ 犧曦㬢爔羲戲嚱❌❌❌❌檥巇嶬桸獻僖嬉嘻禧釐譆熹暿熺熙娭㜯稀俙晞睎豨狶欷唏鵗希 힁〯 ˧˦ 喜憘憙嬉豨唏俙霼 힁〮 ˦ 戲齂哂嚊屭豷燹咥憙喜憘嬉欷唏愾餼䊠旣熂霼墍❌❌摡❌黖 |} === ㅚ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:060 || ㄱ || 굉 ˧ 傀❌❌❌❌瑰瓌璝 굉〮 ˦ 儈會襘檜澮膾鱠獪䯤鬠❌旝鄶劊會廥憒慖幗蔮刏 |- | 5:061 || ㅋ || 쾽 ˧ 恢❌詼盔魁❌悝 쾽〮 ˦ ❌塊蕢蒯❌堁 |- | 5:061 || ㆁ || ᅌᅬᆼ ˧ 嵬峞隗阢桅磑 ᅌᅬᆼ〯 ˧˦ 隗嵬峞䫥頠 ᅌᅬᆼ〮 ˦ 外磑 |- | 5:061 || ㄷ || 됭 ˧ 鎚追❌頧搥槌磓堆❌塠㕍❌❌敦 됭〮 ˦ 祋杸對轛❌碓敦 |- | 5:062 || ㅌ || 툉 ˧ 推蓷焞 툉〯 ˧˦ 腿 툉〮 ˦ 娧脫稅駾蛻兌退𨑧𢓇 |- | 5:063 || ㄸ || 뙹 ˧ 隤頹墤穨尵僓魋 뙹〯 ˧˦ 鐓憝隊 뙹〮 ˦ 兌𩊭銳𠜑奪隊䨴𩅥𩄮薱憝譈懟憞譵鐓駾 |- | 5:063 || ㄴ || 뇡 ˧ 挼捼 뇡〯 ˧˦ 餧餒鯘鮾脮腇腇 뇡〮 ˦ 內 |- | 5:064 || ㅈ || 죙〮 ˦ 最蕞棷粹晬綷❌祽 |- | 5:064 || ㅊ || 쵱 ˧ 崔催縗衰䙑 쵱〯 ˧˦ 漼璀皠洒綷萃 쵱〮 ˦ 襊㝮竄倅萃淬焠𠗚啐啛 |- | 5:065 || ㅉ || 쬥 ˧ 摧漼凗崔㠑磪𡹐 쬥〯 ˧˦ 辠𡽕 쬥〮 ˦ 蕞 |- | 5:065 || ㅅ || 쇵 ˧ 𣯧𦸏挼䪎鞖 쇵〮 ˦ 碎粹誶 |- | 5:065 || ㆆ || ᅙᅬᆼ ˧ 隈❌椳煨䋿偎畏 ᅙᅬᆼ〯 ˧˦ 猥❌椳萎 ᅙᅬᆼ〮 ˦ 薈懀濊 |- | 5:065 || ㅎ || 횡 ˧ 灰❌䝇豗拻 횡〯 ˧˦ 賄悔 횡〮 ˦ ❌噦鐬翽誨晦悔❌靧頮 |- | 5:066 || ㆅ || ᅘᅬᆼ ˧ 回廻茴洄徊瑰槐瘣 ᅘᅬᆼ〯 ˧˦ 瘣壞廆匯滙回 ᅘᅬᆼ〮 ˦ 會禬繪䙡璯潰繪繪䜋聵瞶闠回匯䔇 |- | 5:067 || ㄹ || 룅 ˧ 雷䨓❌罍❌鑘𨯔❌礧轠瓃❌ 룅〯 ˧˦ 磊磥礌礧❌㠥礨壘櫑儡❌❌❌㒦❌蕾 룅〮 ˦ 酹類耒礧壘礌雷㵢攂 |} === ㅐ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:068 || ㄱ || 갱 ˧ 佳街皆偕湝階堦楷喈❌荄痎䕸稭祴該垓畡陔閡絯胲侅晐峐剴祴裓 갱〯 ˧˦ 解改胲❌陔閡 갱〮 ˦ 蓋蓋匃匄丐懈解繲廨解戒誡悈介界堺❌魪玠琾㠹价尬芥疥蚧❌䯰❌屆漑摡槪扢槪㧉剴 |- | 5:070 || ㅋ || 캥 ˧ 揩❌❌❌開 캥〯 ˧˦ 鍇楷愷凱豈鎧塏闓 캥〮 ˦ 磕礚愒❌渴嘅❌揩慨鎧闓欬咳愾 |- | 5:070 || ㆁ || ᅌᅢᆼ ˧ 厓崖涯漄睚倪皚❌凒敱 ᅌᅢᆼ〯 ˧˦ 騃 ᅌᅢᆼ〮 ˦ 艾❌㣻礙硋㝵儗閡 |- | 5:071 || ㄷ || 댕〮 ˦ 帶㿃蔕蹛遞 |- | 5:071 || ㅌ || 탱〮 ˦ 泰太太大忕㥭汰汏蠆𧀱蔕慸 |- | 5:071 || ㄸ || 땡〯 ˧˦ 廌豸豸 땡〮 ˦ 大軑釱汏 |- | 5:072 || ㄴ || 냉 ˧ 能 냉〯 ˧˦ 嬭㛋㚷乃迺鼐 냉〮 ˦ 柰耐𦓎能𣉘奈鼐 |- | 5:072 || ㅂ || 뱅〯 ˧˦ 擺❌捭 뱅〮 ˦ 貝狽伂沛拜扒敗 |- | 5:072 || ㅍ || 팽〮 ˦ 霈沛肺浿派湃㵒 |- | 5:073 || ㅃ || 뺑 ˧ 牌❌❌蠯螷❌排俳 뺑〯 ˧˦ 罷 뺑〮 ˦ 旆沛茷❌柭軷粺❌稗❌❌韛❌❌排糒敗唄 |- | 5:073 || ㅁ || 맹 ˧ 埋貍霾䨪 맹〯 ˧˦ 買 맹〮 ˦ 眛昧沬賣韎霾邁❌勱佅 |- | 5:074 || ㅈ || 쟁 ˧ 齋齊𩱳 쟁〯 ˧˦ 跐 쟁〮 ˦ 債責瘵祭 |- | 5:074 || ㅊ || 챙 ˧ 釵靫扠搋差䡨 챙〮 ˦ 蔡䌨縩瘥差衩𣲾 |- | 5:075 || ㅉ || 쨍 ˧ 柴祡豺犲儕 쨍〮 ˦ 眦❌疵眥砦柴 |- | 5:075 || ㅅ || 생 ˧ 簁篩 생〯 ˧˦ 灑洒躧蹝纚徙 생〮 ˦ 曬灑洒鎩𨦅殺閷煞 |- | 5:076 || ㆆ || ᅙᅢᆼ ˧ 娃哇❌挨唲 ᅙᅢᆼ〯 ˧˦ 矮㾨躷 ᅙᅢᆼ〮 ˦ 藹靄䨠馤壒❌濭隘阨搤噫❌呃嗄餲喝 |- | 5:076 || ㆅ || ᅘᅢᆼ ˧ 諧湝骸膎鞵鞋鮭 ᅘᅢᆼ〯 ˧˦ 蟹蟹獬❌鮭嶰澥解駭絯豥駴夥 ᅘᅢᆼ〮 ˦ 害妎邂解械❌薤❌瀣齘 |- | 5:077 || ㄹ || 랭〮 ˦ 賴瀨籟藾癩襰厲糲蠣賚 |} === ㅙ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:077 || ㄱ || 괭 ˧ 乖❌媧緺騧❌蝸 괭〯 ˧˦ 掛 괭〮 ˦ 卦掛挂絓罣詿怪怪壞夬澮獪㹟狤 |- | 5:078 || ㅋ || 쾡 ˧ ❌華䦱 쾡〮 ˦ 蒯❌蕢塊喟嘳㕟快噲 |- | 5:078 || ㆁ || ᅌᅫᆼ〮 ˦ 聵❌❌ |- | 5:078 || ㅊ || 쵕〮 ˦ 嘬 |- | 5:078 || ㆆ || ᅙᅫᆼ ˧ 蛙鼃洼 |- | 5:078 || ㆅ || ᅘᅫᆼ ˧ 懷櫰槐褢❌❌淮 ᅘᅫᆼ〮 ˦ 壞畵罫繣澅絓話❌躗吳 |} === ㅟ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:079 || ㄱ || 귕 ˧ 嬀潙佹龜騩歸 귕〯 ˧˦ 軌軌軌匭氿漸宄❌晷簋詭垝陒佹❌祪䤥庪庋攱傀鬼 귕〮 ˦ 媿愧❌聭騩攰庪貴瞶劌劂❌撅橛蹶❌❌鱖鱥 |- | 5:080 || ㅋ || 큉 ˧ 虧𧇾𩏣蘬巋 큉〯 ˧˦ 巋蘬 큉〮 ˦ 喟嘳㕟䙡巋缺 |- | 5:081 || ㄲ || 뀡 ˧ 逵㙺夔犪騤戣鍨頯頄馗 뀡〯 ˧˦ 跪 뀡〮 ˦ 匱鐀櫃饋歸蕢䕚簣籄㙺 |- | 5:081 || ㆁ || ᅌᅱᆼ ˧ 危峗峞帷爲韋幃褘湋違闈圍巍嵬犩❌ ᅌᅱᆼ〯 ˧˦ 頠姽峗瓦韙偉瑋煒颹暐韡❌葦 ᅌᅱᆼ〮 ˦ 僞胃❌謂渭蝟猬❌㥜媦緯圍彙❌❌魏犩❌衛熭❌❌轊䡺❌❌彘㻰 |- | 5:082 || ㄷ || 뒹〮 ˦ 轛 |- | 5:082 || ㆆ || ᅙᅱᆼ ˧ 逶倭䴧蜲痿萎委威葳蝛 ᅙᅱᆼ〯 ˧˦ 委骩磈嵬 ᅙᅱᆼ〮 ˦ 餧❌委骩尉慰蔚霨罻畏威 |- | 5:083 || ㅎ || 휭 ˧ 麾戲摩撝暉煇煒輝揮楎椲翬❌褘徽幑㫎 휭〯 ˧˦ 毁毁燬䅏❌毇❌烜䃣蟲虺蘬❌❌燬卉 휭〮 ˦ 毁諱卉 |- | 5:084 || ㅇ || 윙〯 ˧˦ 蔿蘤䦱❌❌ 윙〮 ˦ 位爲 |} === ㆌ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:084 || ㄱ || ᄀᆔᆼ ˧ 規槼摫瞡❌雉鳺 ᄀᆔᆼ〯 ˧˦ 癸湀 ᄀᆔᆼ〮 ˦ 季 |- | 5:084 || ㅋ || ᄏᆔᆼ ˧ 闚窺 ᄏᆔᆼ〯 ˧˦ 跬頃蹞窺❌頍頯 |- | 5:085 || ㄲ || ᄁᆔᆼ ˧ 葵楑鄈 ᄁᆔᆼ〯 ˧˦ 揆 ᄁᆔᆼ〮 ˦ 悸 |- | 5:085 || ㄷ || ᄃᆔᆼ ˧ 腄追䨨 |- | 5:085 || ㄸ || ᄄᆔᆼ ˧ 椎桘鎚錘槌甀魋椎鬌 ᄄᆔᆼ〮 ˦ 縋䋘槌膇㾽硾倕磓墜錘腄甀墜隊隧❌懟譵 |- | 5:086 || ㄴ || ᄂᆔᆼ〮 ˦ 諉 |- | 5:086 || ㅈ || ᄌᆔᆼ ˧ 劑檇㰎崔墔隹崒❌厜隹萑鵻騅錐 ᄌᆔᆼ〯 ˧˦ 觜❌嘴❌嶉捶棰錘箠菙 ᄌᆔᆼ〮 ˦ 醉檇雋惴諈錘 |- | 5:087 || ㅊ || ᄎᆔᆼ ˧ 吹龡炊推蓷衰 ᄎᆔᆼ〯 ˧˦ 趡揣 ᄎᆔᆼ〮 ˦ 翠臎吹䶴龡出 |- | 5:087 || ㅉ || ᄍᆔᆼ〮 ˦ 萃瘁悴顇踤 |- | 5:087 || ㅅ || ᄉᆔᆼ ˧ 綏浽荽荾䒘雖睢濉衰榱 ᄉᆔᆼ〯 ˧˦ 髓髓❌䯝䯝❌膸瀡靃❌巂嶲水準 ᄉᆔᆼ〮 ˦ 邃粹睟誶祟帥帨率 |- | 5:087 || ㅆ || ᄊᆔᆼ ˧ 隨隋遺垂陲倕❌腄誰唯譙脽 ᄊᆔᆼ〯 ˧˦ 菙❌ ᄊᆔᆼ〮 ˦ 遂燧❌鐩澻㴚襚❌璲繸❌❌隧❌隊旞❌❌彗篲穗❌穟瑞睡倕 |- | 5:089 || ㆆ || ᅙᆔᆼ〮 ˦ 恚烓 |- | 5:089 || ㅎ || ᄒᆔᆼ ˧ 墮❌❌隳隋綏觽鐫鐫睢倠 ᄒᆔᆼ〮 ˦ 睢隋綏墮挼侐 |- | 5:090 || ㅇ || ᄋᆔᆼ ˧ 惟維唯濰遺壝蠵❌❌觿 ᄋᆔᆼ〯 ˧˦ 洧鮪痏䔺❌芛芟唯踓趡壝 ᄋᆔᆼ〮 ˦ 遺❌壝瞶蜼 |- | 5:090 || ㄹ || ᄅᆔᆼ ˧ 羸纍縲累欙樏㹎❌虆藟❌蘲蔂絫 ᄅᆔᆼ〯 ˧˦ 壘藟虆蘽櫐讄❌❌❌礧❌㠥❌礨礨❌蠝❌累樏誄蜼猚 ᄅᆔᆼ〮 ˦ 類禷蘱率淚累絫 |- | 5:092 || ㅿ || ᅀᆔᆼ ˧ 甤蕤苼痿緌綏䅑䅗桵㮃 ᅀᆔᆼ〯 ˧˦ 繠橤蘂❌蕊 |} === ㅖ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:008 || ㄱ || 곙 ˧ 雞稽𮃛笄䈕枅𣓖卟乩 곙〮 ˦ 罽𦇧𣯅𦆡𣽄猘狾計繼㡭繫毄係❌髻結𨜒薊 |- | 6:008 || ㅋ || 콍 ˧ 谿溪磎嵠鸂䳶 콍〯 ˧˦ 啓棨𢧊綮启稽 콍〮 ˦ 愒憇𢠾偈❌揭䔾甈契栔鍥㓶挈 |- | 6:009 || ㄲ || 꼥〮 ˦ 偈碣嵑 |- | 6:009 || ㆁ || ᅌᅨᆼ〯 ˧˦ 掜 ᅌᅨᆼ〮 ˦ ❌甈乂刈㣻艾 |- | 6:010 || ㄷ || 뎽 ˧ 氐互低仾詆呧柢羝牴隄䧑堤鞮䩚磾 뎽〯 ˧˦ 邸䣌柢詆呧阺坻弤抵掋牴觝軝疧疷底㡳氐提堤底 뎽〮 ˦ 帝諦諟𧫚締蔕䗖柢泜氐疐嚔㗣 |- | 6:011 || ㅌ || 톙 ˧ 梯鷈鷉鵜𩀗 톙〯 ˧˦ 體軆躰涕緹紙醍 톙〮 ˦ 替暜朁涕洟鬀鬄剃殢揥楴𧝐褅裼薙雉𡲕傺袲 |- | 6:012 || ㄸ || 똉 ˧ 題提媞偍姼禔褆緹鞮睼瑅騠踶鶗嗁啼諦㖒㖷渧蹏蹄締綈䬾稊𦯔稺䄺𧀾銻𧋘鵜鴺罤䨑荑梯鮧鯷桋鴺㡗折 똉〯 ˧˦ 弟悌娣遞递𮞏 똉〮 ˦ 第弟悌娣睇眱珶遞迭递𮞏遰褅締杕軑釱踶提題鬄髢錫鶗鷤逮棣滯㿃䐭彘 |- | 6:014 || ㄴ || 녱 ˧ 泥埿臡 녱〯 ˧˦ 禰祢檷鑈鈮濔瀰薾爾泥 녱〮 ˦ 泥 |- | 6:015 || ㅂ || 볭 ˧ 篦豍❌㡙鎞❌狴❌ 볭〮 ˦ 閉箄嬖蔽草弊弊鷩鄨廢癈祓苃茀 |- | 6:015 || ㅍ || 폥 ˧ ❌批錍鈚漂 폥〮 ˦ 媲睤❌䁹辟壀埤僻濞淠潎❌肺杮 |- | 6:016 || ㅃ || 뼹 ˧ 鼙鞞𧯿椑㼰膍 뼹〯 ˧˦ 陛陛梐狴陛髀䯗䏶❌脾肶 뼹〮 ˦ 弊獘斃弊幣❌❌薜蘗❌吠犻❌茷 |- | 6:017 || ㅁ || 몡 ˧ 迷麛麑❌❌ 몡〯 ˧˦ 米眯䋛❌洣 몡〮 ˦ 袂 |- | 6:017 || ㅈ || 졩 ˧ 齎䝴韲齏❌虀懠❌擠躋隮齊❌❌ 졩〯 ˧˦ 濟❌㧗 졩〮 ˦ 霽擠濟❌祭際穄制製䱥鰶㫼晣晢淛浙折 |- | 6:018 || ㅊ || 촁 ˧ 妻萋霋凄淒悽緀 촁〯 ˧˦ 泚玼萋緀 촁〮 ˦ 切砌墄❌妻掣❌摯觢挈懘滯❌ |- | 6:018 || ㅉ || 쪵 ˧ 齊臍蠐 쪵〯 ˧˦ 薺齊癠鮆鱭 쪵〮 ˦ 嚌懠穧❌劑齊薺癠齌眥眦 |- | 6:019 || ㅅ || 솅 ˧ 西栖棲犀屖遟凘澌嘶撕 솅〯 ˧˦ 洗灑 솅〮 ˦ 細壻壻棲世貰勢埶殺閷 |- | 6:020 || ㅆ || 쏑 ˧ 栘 쏑〮 ˦ 誓逝遰遞筮❌噬澨澨忕 |- | 6:020 || ㆆ || ᅙᅨᆼ ˧ 鷖繄䃜瑿㙠黳 ᅙᅨᆼ〮 ˦ 瘞❌餲医瞖繄翳蘙嫕瘱曀㙪㙠殪縊 |- | 6:020 || ㅎ || 혱 ˧ 醯❌䤈橀 |- | 6:021 || ㆅ || ᅘᅨᆼ ˧ 兮❌奚㜎傒蹊❌騱貕豀謑鼷嵇 ᅘᅨᆼ〯 ˧˦ 徯諐蹊❌傒謑謑奚 ᅘᅨᆼ〮 ˦ 系繫係結禊妎盻 |- | 6:021 || ㅇ || 옝 ˧ 倪齯鯢蜺䘽輗❌棿麑猊猊霓兒 옝〮 ˦ 曳洩❌袣❌泄呭詍㖂䛖枻栧抴跇裔❌㵝㵩㳿㵩勩詣栺睨倪堄帠羿❌寱藝蓺❌槸褹袂囈 |- | 6:022 || ㄹ || 롕 ˧ 黎❌❌犁黧藜蔾瓈邌梨驪鸝蠡 롕〯 ˧˦ 禮澧醴鱧蠡䗍劙盠欐 롕〮 ˦ 麗䕻儷儷❌欐攦戾唳淚綟棙悷❌蜧茘❌蛠❌❌隷隷劙蠫❌㓯盭沴離例列栵迾裂厲厲礪❌❌糲爄勵蠣❌癘❌㾐❌ |} === ㆋ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:024 || ㄱ || ᄀᆒᆼ ˧ 圭窐❌❌❌閨袿邽蠲 ᄀᆒᆼ〮 ˦ 桂筀䳏 |- | 6:024 || ㅋ || ᄏᆒᆼ ˧ 睽聧奎暌刲㨒 |- | 6:025 || ㄷ || ᄃᆒᆼ〮 ˦ 綴餟醊腏畷錣 |- | 6:025 || ㅈ || ᄌᆒᆼ〮 ˦ 蕝蕞贅 |- | 6:025 || ㅊ || ᄎᆒᆼ〮 ˦ 脃膬毳橇竁喙 |- | 6:025 || ㅅ || ᄉᆒᆼ〮 ˦ 歲繐❌稅帨帥蛻涗裞䬽鐫說 |- | 6:025 || ㅆ || ᄊᆒᆼ〮 ˦ 篲 |- | 6:025 || ㆆ || ᅙᆒᆼ ˧ 烓❌ ᅙᆒᆼ〮 ˦ 穢薉濊獩 |- | 6:026 || ㅎ || ᄒᆒᆼ〮 ˦ 嘒嚖❌暳噦喙𣨶𤸁𠿔顪噦翽 |- | 6:026 || ㆅ || ᅘᆒᆼ ˧ 攜㩗携觿鑴蠵酅❌巂畦 ᅘᆒᆼ〮 ˦ 慧轊惠蕙譓憓 |- | 6:026 || ㅇ || ᄋᆒᆼ〮 ˦ 叡睿銳梲 |- | 6:027 || ㅿ || ᅀᆒᆼ〮 ˦ 汭內枘芮蜹 |} === ㅗ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:027 || ㄱ || 공 ˧ 孤❌觚❌柧軱呱❌箛䉉苽菰罛❌姑酤沽蛄鴣䧸辜❌❌橭❌盬夃 공〯 ˧˦ 古詁故估牯沽酤罟苦楛鼓❌瞽鼓股❌骰羖❌蠱監賈夃 공〮 ˦ 顧雇固凅錮鯝稒㧽痼故酤沽詁蠱 |- | 6:028 || ㅋ || 콩 ˧ 枯刳恗 콩〯 ˧˦ 苦䇢 콩〮 ˦ 庫袴絝胯跨❌ |- | 6:029 || ㆁ || ᅌᅩᆼ ˧ 吾浯梧鼯❌❌郚吳㻍珸鋘鋙 ᅌᅩᆼ〯 ˧˦ 五伍午迕仵旿 ᅌᅩᆼ〮 ˦ 誤悞悟晤旿晤捂梧忤牾午蘁逜寤❌遻❌迕俉愕 |- | 6:029 || ㄷ || 동 ˧ 都闍堵 동〯 ˧˦ 覩堵❌陼暏賭❌楮肚士 동〮 ˦ 妒妬奼㓃宅詫秅秺蠧螙❌斁❌睪 |- | 6:030 || ㅌ || 통 ˧ 稌 통〯 ˧˦ 土吐稌 통〮 ˦ 兎鵵菟吐 |- | 6:031 || ㄸ || 똥 ˧ 徒途墿峹𡷣荼𣘻余涂捈駼鵌鷋䅷䔑塗屠瘏菟𧈋檡兎圖鍍 똥〯 ˧˦ 杜荰土赭𢾅剫 똥〮 ˦ 度渡𣳥鍍塗 |- | 6:031 || ㄴ || 농 ˧ 奴㚢仅孥帑駑笯砮 농〯 ˧˦ 怒弩砮努 농〮 ˦ 笯怒㣽𢘂 |- | 6:032 || ㅂ || 봉 ˧ 逋餔哺誧晡 봉〯 ˧˦ 補䋠❌圃甫❌譜諩 봉〮 ˦ 布尃抪圃 |- | 6:032 || ㅍ || 퐁 ˧ 鋪痡鯆❌❌抪 퐁〯 ˧˦ 普浦誧溥 퐁〮 ˦ 怖鋪誧 |- | 6:033 || ㅃ || 뽕 ˧ 蒲莆蒱酺匍扶䒀瓿 뽕〯 ˧˦ 簿部蔀 뽕〮 ˦ 步❌荹捕哺餔䊇❌酺脯鞴 |- | 6:033 || ㅁ || 몽 ˧ 模橅❌❌謨謩嫫❌❌膜摹摸撫墓母 몽〯 ˧˦ 姥莽茻❌姆 몽〮 ˦ 暮慕募墓謨慔 |- | 6:034 || ㅈ || 종 ˧ 租蒩菹𦼬𧂚𧀽𦵔𦯓苴 종〯 ˧˦ 祖珇駔組阻岨俎柤𤕲齟詛 종〮 ˦ 作詛𥛜謯作 |- | 6:035 || ㅊ || 총 ˧ 麤麆粗觕初 총〯 ˧˦ 楚礎濋憷❌ 총〮 ˦ 措厝錯醋楚 |- | 6:035 || ㅉ || 쫑 ˧ 徂且鉏鋤助耡 쫑〯 ˧˦ 粗麆𧇿 쫑〮 ˦ 祚胙飵阼葄助耡鉏莇 |- | 6:036 || ㅅ || 송 ˧ 蘇穌㢝酥𦣑𨢭蔬疏疎𤕠梳𣐌𣓜疋綀釃斯 송〯 ˧˦ 所𢨷䝪糈 송〮 ˦ 素愫榡傃嗉訴𧪜愬㴑遡溯泝塑塐疏𥿇 |- | 6:037 || ㆆ || ᅙᅩᆼ ˧ 烏於杇圬釫❌汙汚於嗚惡 ᅙᅩᆼ〯 ˧˦ 塢䃖埡瑦鄔 ᅙᅩᆼ〮 ˦ 汙洿盓❌惡䛩䜑堊嗚 |- | 6:037 || ㅎ || 홍 ˧ 呼虖謼嘑滹❌淲❌❌泘膴幠芋 홍〯 ˧˦ 虎琥滸滹許 홍〮 ˦ 謼嘑呼戽 |- | 6:038 || ㆅ || ᅘᅩᆼ ˧ 胡❌𠴱箶❌湖瑚餬❌䭌醐❌糊粘❌❌❌䉿❌鶘乎虖壷弧狐❌瓠葫 ᅘᅩᆼ〯 ˧˦ 戶昈雇❌扈❌怙❌岵祜酤楛鄠❌下芐芦嫭嫮橭 ᅘᅩᆼ〮 ˦ 護濩頀䪝穫互枑冱瓠嫮嫭涸 |- | 6:039 || ㄹ || 롱 ˧ 盧蘆籚廬鑢爐櫨鱸壚艫轤瓐黸獹瀘矑纑顱髗❌❌玈❌❌ 롱〯 ˧˦ 魯櫓櫓㯭艣虜擄鹵滷塷瀂❌ 롱〮 ˦ 路璐露簬❌鷺❌鸕鴼潞輅賂 |} === ㅏ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:041 || ㄱ || 강 ˧ 歌謌❌戨❌牁哥柯鴚舸渮荷渮嘉佳加笳茄鴐駕珈耞跏迦痂枷葭麚豭猳家 강〯 ˧˦ 哿笴舸菏荷❌賈檟榎夏假徦嘏椵斝斝 강〮 ˦ 箇個介駕架榢枷稼嫁幏假叚下斝斝價賈 |- | 6:042 || ㅋ || 캉 ˧ 珂砢軻呿 캉〯 ˧˦ 可岢軻坷 캉〮 ˦ 軻坷髂䯊❌ |- | 6:043 || ㆁ || ᅌᅡᆼ ˧ 莪峨峨我娥俄哦睋蛾䖸鵝❌䳘鵞牙芽枒㧎衙涯厓 ᅌᅡᆼ〯 ˧˦ 我騀硪峨雅鴉庌牙疋 ᅌᅡᆼ〮 ˦ 餓訝迓御輅衙庌砑牙 |- | 6:044 || ㄷ || 당 ˧ 多多奓 당〯 ˧˦ 嚲癉哆打 당〮 ˦ 癉憚咤喥䖳㓃宅❌奼哆奓 |- | 6:044 || ㅌ || 탕 ˧ 佗他拕拖扡蛇詑訑 탕〯 ˧˦ 姹 탕〮 ˦ 詫❌咤侘差 |- | 6:045 || ㄸ || 땅 ˧ 駝駞它沱沲池陀岮陁阤跎❌佗紽酡鮀迤池鼉鱓鱣驒馱秅茶𣘻涂塗 땅〯 ˧˦ 拕拖扡佗柁舵䑨杕袉沱❌❌沲阤陀陊爹 땅〮 ˦ 馱他大 |- | 6:046 || ㄴ || 낭 ˧ 那難單儺拏挐笯 낭〯 ˧˦ 娜那袲橠難❌旎儺 낭〮 ˦ 柰那哪㖠 |- | 6:047 || ㅂ || 방 ˧ 波碆番僠嶓巴笆芭豝❌羓鈀 방〯 ˧˦ 跛❌簸播把 방〮 ˦ 播譒磻簸霸伯灞壩䃻靶弝杷欛 |- | 6:048 || ㅍ || 팡 ˧ 頗❌坡岥❌陂❌玻葩苩舥芭 팡〯 ˧˦ 頗叵 팡〮 ˦ 破帊帕袙怕❌❌ |- | 6:048 || ㅃ || 빵 ˧ 婆鄱番皤❌番杷爬把琶 빵〮 ˦ 縛杷䆉❌罷 |- | 6:049 || ㅁ || 망 ˧ 摩擵磨❌麼❌䯢魔劘攠麻菻蟆䗫蟇 망〯 ˧˦ 麼䯢馬 망〮 ˦ 磨禡貊伯榪罵䣕䣖鬕 |- | 6:049 || ㅈ || 장 ˧ 樝❌𣕈柤齟 장〯 ˧˦ 左𡯛鮓䱹𩽫苴苲 장〮 ˦ 左佐𡯛作詐醡笮苲榨𨣮𨢦溠咋 |- | 6:050 || ㅊ || 창 ˧ 蹉嵯磋瑳搓差叉杈靫扠差艖舣鎈 창〯 ˧˦ 瑳 창〮 ˦ 磋蹉差汊衩 |- | 6:051 || ㅉ || 짱 ˧ 醝鹺艖𦪸䑘㽨瘥𣩈嵳𥰭齹䣜鄼 |- | 6:051 || ㅅ || 상 ˧ 娑挱莎莏挲沙䤬𨪍桫髿𣯌犠獻戲傞𠈱沙𣲡砂紗𦀟鯊㲚髿硰 상〯 ˧˦ 娑灑洒葰傻 상〮 ˦ 些娑逤嗄𣣺沙 |- | 6:052 || ㅆ || 쌍 ˧ 槎楂苴 쌍〯 ˧˦ 槎𠞊柞 쌍〮 ˦ 乍䄍蓌 |- | 6:053 || ㆆ || ᅙᅡᆼ ˧ 阿娿痾䋪䋍鴉鴉丫啞亞 ᅙᅡᆼ〯 ˧˦ 娿婀妸❌阿猗啞瘂❌ ᅙᅡᆼ〮 ˦ 亞惡啞稏婭 |- | 6:053 || ㅎ || 항 ˧ 訶苛何呵㰤呀谺岈❌鰕 항〯 ˧˦ 㰤閜 항〮 ˦ 呵罅㙤釁❌❌嚇赫哧謑 |- | 6:054 || ㆅ || ᅘᅡᆼ ˧ 何荷河苛遐徦假蕸霞瑕鍜碬蝦鰕騢假赮 ᅘᅡᆼ〯 ˧˦ 荷何❌抲苛下夏廈 ᅘᅡᆼ〮 ˦ 賀❌荷何暇假嘉下芐夏 |- | 6:055 || ㄹ || 랑 ˧ 羅蘿籮❌鑼欏囉饠 랑〯 ˧˦ 砢❌㦬❌攞邏 |} === ㅑ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:055 || ㄱ || 걍 ˧ 迦 |- | 6:055 || ㅋ || 컁 ˧ 呿 |- | 6:055 || ㄲ || 꺙 ˧ 伽茄 |- | 6:055 || ㅈ || 쟝 ˧ 嗟罝𦋽遮庶 쟝〯 ˧˦ 姐她媎者赭堵 쟝〮 ˦ 借藉唶柘䂞蔗𤯈𤯋鷓蟅䗪炙 |- | 6:056 || ㅊ || 챵 ˧ 車 챵〯 ˧˦ 且奲撦䰩奓哆拸䞣 |- | 6:056 || ㅉ || 쨩〯 ˧˦ 抯飷 쨩〮 ˦ 藉蒩耤 |- | 6:057 || ㅅ || 샹 ˧ 些𡭟奢賖畬 샹〯 ˧˦ 寫𣞐瀉捨舍 샹〮 ˦ 卸瀉舍赦厙 |- | 6:057 || ㅆ || 썅 ˧ 邪耶斜䔑𦳃闍堵蛇虵鉈 썅〯 ˧˦ 䄬社 썅〮 ˦ 謝榭㴬射䠶麝貰 |- | 6:058 || ㅇ || 양 ˧ 邪釾鋣鎁枒椰椰耶爺斜 양〯 ˧˦ 野野也冶蠱 양〮 ˦ 夜射 |- | 6:058 || ㅿ || ᅀᅣᆼ〯 ˧˦ 惹❌若 |} === ㅘ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:058 || ㄱ || 광 ˧ 戈過撾渦❌鍋鈛❌瓜媧騧❌蝸緺 광〯 ˧˦ 果菓惈❌裹蜾裸寡❌另剮 광〮 ˦ 過裹燴 |- | 6:059 || ㅋ || 쾅 ˧ 科蝌窠薖簻誇侉胯跨姱恗夸❌荂華 쾅〯 ˧˦ 顆堁果骻胯跨㡁袴銙❌錁❌夸 쾅〮 ˦ 課堁跨❌夸胯袴❌ |- | 6:060 || ㄲ || 꽝 ˧ 瘸 |- | 6:060 || ㆁ || ᅌᅪᆼ ˧ 吪❌囮繇鈋譌訛 ᅌᅪᆼ〯 ˧˦ 瓦 ᅌᅪᆼ〮 ˦ 臥 |- | 6:060 || ㄷ || 돵 ˧ 檛薖撾 돵〯 ˧˦ 朶揣挅捶㪜崜埵𥠄鬌 |- | 6:061 || ㅌ || 퇑 ˧ 詑 퇑〯 ˧˦ 妥綏𢼻隋橢䲊墮媠 퇑〮 ˦ 唾涶佗扡拕拖大 |- | 6:061 || ㄸ || 뙁〯 ˧˦ 惰嶞墮墯隓𡐦垜𨹃䅜 뙁〮 ˦ 惰憜媠墮𢞑𢢠 |- | 6:062 || ㄴ || 놩 ˧ 捼挼 놩〮 ˦ 愞懦偄𦓏稬糯𤲬堧壖 |- | 6:062 || ㅈ || 좡 ˧ 髽 좡〮 ˦ 挫蓌夎䟶 |- | 6:062 || ㅊ || 촹〯 ˧˦ 脞 촹〮 ˦ 剉挫莝摧 |- | 6:062 || ㅉ || 쫭 ˧ 矬痤 쫭〯 ˧˦ 坐 쫭〮 ˦ 坐座 |- | 6:063 || ㅅ || 솽 ˧ 蓑莎梭𥭟唆 솽〯 ˧˦ 鎖鏁瑣璅䵀葰 |- | 6:063 || ㆆ || ᅙᅪᆼ ˧ 倭踒渦窩窊窳溛窪窐洼❌哇娃蛙䵷汙 ᅙᅪᆼ〯 ˧˦ 婐果 ᅙᅪᆼ〮 ˦ 涴汙堁 |- | 6:064 || ㅎ || 황 ˧ 鞾靴❌❌㞜花華❌ 황〯 ˧˦ 火 황〮 ˦ 貨化 |- | 6:064 || ㆅ || ᅘᅪᆼ ˧ 和龢鉌禾華崋譁嘩驊❌鋘釫鏵 ᅘᅪᆼ〯 ˧˦ 禍❌輠❌夥❌髁踝裸裸觟 ᅘᅪᆼ〮 ˦ 和華樺檴嬅摦擭獲鱯㕦 |- | 6:065 || ㄹ || 뢍 ˧ 鸁䯁騾臝蠃螺蠡蝸❌❌鏍虆蔂❌❌覼 뢍〯 ˧˦ 裸裸果臝儽蠃蓏❌蠡❌瘰攭 뢍〮 ˦ 邏摞 |} === ㅜ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:066 || ㄱ || 궁 ˧ 拘佝句❌跔駒驕痀俱㪺❌仇捄 궁〯 ˧˦ 矩榘距句拒枸蒟萬楀踽偊鄅 궁〮 ˦ 屨鞻句絇蒟瞿懼 |- | 6:067 || ㅋ || 쿵 ˧ 區驅毆敺❌歐嶇軀❌鮬摳 쿵〯 ˧˦ 齲踽 쿵〮 ˦ 驅 |- | 6:067 || ㄲ || 꿍 ˧ 劬朐❌絇句❌❌䋧軥枸鼩翑句臞癯躣躩忂灈懼戵鑺欋衢瞿鸜 꿍〯 ˧˦ 寠窶 꿍〮 ˦ 懼瞿具❌❌ |- | 6:068 || ㆁ || ᅌᅮᆼ ˧ 虞❌澞鸆麌娛禺愚隅嵎髃腢喁齵堣鍝于竽𥫡迂盂❌杅❌玗釪汙邘❌雩❌謣❌ ᅌᅮᆼ〯 ˧˦ 麌❌俁㒁羽禹䨞萭瑀偊宇㝢㡰❌雨 ᅌᅮᆼ〮 ˦ 遇寓庽禺虞芌芋𩁹吁羽雨 |- | 6:070 || ㅂ || 붕 ˧ 膚❌夫玞扶䄮鈇柎不❌枎跗趺附❌枹 붕〯 ˧˦ 甫父莆黼❌脯俌簠❌府腑俯斧鈇 붕〮 ˦ 付傅❌賦富 |- | 6:071 || ㅍ || 풍 ˧ 敷傳鋪尃溥懯孚莩罦䍖❌稃粰❌❌桴俘郛垺𡎽苻荴紨泭柎❌怤荂❌旉華麩麱䴸鄜 풍〯 ˧˦ 撫❌拊柎弣 풍〮 ˦ 赴訃仆踣掊䞳趏簠副報 |- | 6:072 || ㅃ || 뿡 ˧ 扶蚨颫夫❌芙符苻❌鳧瓿 뿡〯 ˧˦ 父㕮駁輔䩉❌鬴釜❌秿❌滏腐焤 뿡〮 ˦ 附胕柎付駙䮛鮒❌柎坿跗傅賻負負萯偩婦 |- | 6:073 || ㅁ || 뭉 ˧ 無亡蕪䍢❌璑毋巫誣 뭉〯 ˧˦ 武鵡母碔珷璑舞儛廡❌膴嫵斌憮㒇嘸甒❌❌橆蕪侮侮務䍙堥 뭉〮 ˦ 務婺瞀騖鶩霧 |- | 6:074 || ㅊ || 충 ˧ 芻䅳 |- | 6:074 || ㅉ || 쭝 ˧ 雛䅳媰 |- | 6:074 || ㅅ || 숭 ˧ 毹毺㲣𡨙氀 숭〮 ˦ 數捒 |- | 6:074 || ㆆ || ᅙᅮᆼ ˧ 紆汙迂盓陓 ᅙᅮᆼ〯 ˧˦ 傴痀❌嫗噢 ᅙᅮᆼ〮 ˦ 嫗饇歐燠噢 |- | 6:075 || ㅎ || 훙 ˧ 訏吁盱❌芋❌旴晇冔❌昫煦蓲姁喣嘔呴謳 훙〯 ˧˦ 詡栩珝訏冔❌姁昫喣煦煦膴咻 훙〮 ˦ 煦昫呴欨咻休䣱酗 |- | 6:076 || ㄹ || 룽 ˧ 慺膢褸漊鏤氀❌蔞𦭯婁 룽〯 ˧˦ 縷僂軁漊嶁㟺褸簍蔞婁 룽〮 ˦ 屢婁僂 |} === ㅠ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:077 || ㄷ || 듕 ˧ 株誅蛛跦邾禂 듕〯 ˧˦ 𪐴拄柱 듕〮 ˦ 駐軴柱住𨙦𩦛 |- | 6:077 || ㅌ || 튱 ˧ 貙 |- | 6:077 || ㄸ || 뜡 ˧ 廚躕裯幮幬趎 뜡〯 ˧˦ 柱 뜡〮 ˦ 住 |- | 6:078 || ㅈ || 즁 ˧ 諏𧩻娵朱珠侏咮祩袾 즁〯 ˧˦ 主宔砫袾麈炷枓斗 즁〮 ˦ 足注屬主註炷鉒䪒澍霔咮𠰍鑄馵 |- | 6:078 || ㅊ || 츙 ˧ 趨𨃘趣騶取樞姝 츙〯 ˧˦ 取 츙〮 ˦ 娶趣趨 |- | 6:079 || ㅉ || 쯍〯 ˧˦ 聚 쯍〮 ˦ 聚鄹𡒍㝡 |- | 6:079 || ㅅ || 슝 ˧ 須𩓣𥪙𥪥䇕𡡓鬚需繻緰㡏𦅎𪋯輸隃鄃 슝〯 ˧˦ 數籔簍藪 슝〮 ˦ 戍隃輸束 |- | 6:080 || ㅆ || 쓩 ˧ 殊銖洙㼡茱殳杸 쓩〯 ˧˦ 豎𠐊侸裋𧞫樹 쓩〮 ˦ 樹澍裋𧞫 |- | 6:080 || ㅇ || 융 ˧ 兪逾踰𧼯窬瘉瘐𨵦渝楡揄𦩞羭蝓𧍳堬瑜牏褕喩愉婾愈睮覦歈㼶臾㬰諛楰腴㥚萸 융〯 ˧˦ 庾㢏㔱楰㥚斞斔愉瘐瘉臾愈瘉癒貐窳寙 융〮 ˦ 裕䘱欲慾籲諭喩覦兪瘉 |- | 6:082 || ㅿ || ᅀᅲᆼ ˧ 儒𠍶嚅吺咮懦愞濡𣽉醹襦𧝄 ᅀᅲᆼ〯 ˧˦ 乳醹㳶 ᅀᅲᆼ〮 ˦ 孺𡦗乳 |} === ㅓ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:083 || ㄱ || 겅 ˧ 居㞐琚❌裾据椐鶋❌車 겅〯 ˧˦ 擧莒筥❌籧❌柜欅弆 겅〮 ˦ 據鐻踞倨裾鋸據居擧 |- | 6:083 || ㅋ || 컹 ˧ 墟丘嶇㠊袪祛佉胠阹呿抾魼鱋去 컹〯 ˧˦ 去麮胠 컹〮 ˦ 去㰦呿胠 |- | 6:084 || ㄲ || 껑 ˧ 渠蕖❌❌❌磲❌蘧遽籧鐻❌璩璖❌醵䣰腒 껑〯 ˧˦ 巨鉅詎渠距❌❌拒歫蚷駏❌炬❌秬岠怇苣虡簴❌❌鐻 껑〮 ˦ 遽蘧懅醵䣰勮詎巨渠 |- | 6:085 || ㆁ || ᅌᅥᆼ ˧ 魚䁩❌䐳❌漁䱷䰻齬衙 ᅌᅥᆼ〯 ˧˦ 語齬鋙❌峿敔梧衙圄圉禦御蘌❌❌❌ ᅌᅥᆼ〮 ˦ 御御禦衙圉語䱷漁 |- | 6:086 || ㆆ || ᅙᅥᆼ ˧ 於淤唹 ᅙᅥᆼ〮 ˦ 飫饇❌❌棜淤❌瘀菸 |- | 6:086 || ㅎ || 헝 ˧ 虛虗歔噓吁喣呴驉❌魖 헝〯 ˧˦ 許 헝〮 ˦ 噓 |} === ㅕ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:087 || ㄷ || 뎡 ˧ 豬猪䐗櫫瀦潴 뎡〯 ˧˦ 貯𡪄著紵䘢宁 뎡〮 ˦ 著 |- | 6:087 || ㅌ || 텽 ˧ 攄捈樗櫖摴摢㻬 텽〯 ˧˦ 楮柠褚 텽〮 ˦ 絮 |- | 6:088 || ㄸ || 뗭 ˧ 除篨滁筡著躇屠儲宁 뗭〯 ˧˦ 宁佇竚紵𦂂羜泞眝坾杼拧䇡抒苧 뗭〮 ˦ 箸櫡躇著宁除 |- | 6:088 || ㄴ || 녕 ˧ 袽帤挐拏 녕〯 ˧˦ 女 녕〮 ˦ 女 |- | 6:089 || ㅈ || 졍 ˧ 苴蒩且蛆沮諸䃴蠩 졍〯 ˧˦ 苴疽䰞𩱰𤑨煮渚陼庶 졍〮 ˦ 怚姐𢚆沮苴翥䬡庶 |- | 6:089 || ㅊ || 쳥 ˧ 疽砠❌沮趄跙狙雎苴 쳥〯 ˧˦ 且杵処處 쳥〮 ˦ 覰䁦狙蜡處 |- | 6:090 || ㅉ || 쪙〯 ˧˦ 沮咀跙 |- | 6:090 || ㅅ || 셩 ˧ 胥諝㥠胥糈稰蝑湑書𦘠舒豫𪅰紓䋡悆忬瑹荼 셩〯 ˧˦ 諝醑湑稰胥暑黍鼠癙 셩〮 ˦ 絮恕庶 |- | 6:091 || ㅆ || 쎵 ˧ 徐邪蜍蠩 쎵〯 ˧˦ 敍漵㵰序䦽㘧茅𣏗杼藇𨣦鱮魣嶼緖紓墅野杼茅𣏗桃 쎵〮 ˦ 署𥌓暏 |- | 6:092 || ㅇ || 영 ˧ 余予畬❌畭蜍餘與歟欤懙❌㦛旟璵㼂譽鸒輿❌舁伃妤茅雓 영〯 ˧˦ 與❌歟予❌ 영〮 ˦ 豫舒余與鸒譽礜藇蕷穥歟輿預忬澦❌悆 |- | 6:093 || ㄹ || 령 ˧ 臚❌盧膚驢䮫廬慮藘閭櫚 령〯 ˧˦ 呂呂侶梠膂旅❌祣❌臚穭稆儢❌ 령〮 ˦ 慮爈❌䥨鋁❌濾勴錄窶 |- | 6:094 || ㅿ || ᅀᅧᆼ ˧ 如鴐茹絮洳 ᅀᅧᆼ〯 ˧˦ 汝籹❌女茹 ᅀᅧᆼ〮 ˦ 茹洳如 |} {{단원 안내|책=동국정운 색인|상위=동국정운 색인 |이전=끝소리 ㅱ |다음=끝소리 ㆁ}} [[분류:동국정운]] 8lrobknlivwq7unxvtpj9yhdbyi8zla 45132 45125 2024-10-31T02:49:59Z 2600:1700:C76:9010:22A7:9C6B:AF28:E81F /* ㅖ */ 45132 wikitext text/x-wiki === ㆍ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:008 || ㅈ || ᄌᆞᆼ ˧ 貲訾𤺒訿觜髭𩑽鄑咨諮資䆅姿粢秶澬姕𪗋齊𧞓齍璾齎玆茲孳滋嵫鎡仔孜耔鼒 ᄌᆞᆼ〯 ˧˦ 紫訿𤺒啙呰訾跐㧗姊秭子耔芓仔杍梓榟 ᄌᆞᆼ〮 ˦ 恣積𥡯㧘柴 |- | 5:009 || ㅊ || ᄎᆞᆼ ˧ 雌䳄郪趑次𨀥𨒮❌ ᄎᆞᆼ〯 ˧˦ 此泚佌𠈈𢇌玼皉 ᄎᆞᆼ〮 ˦ 刺𧧒庛❌𢉪次佽㐸絘䯸 |- | 5:010 || ㅉ || ᄍᆞᆼ ˧ 慈㤵磁鶿鷀鴜茲茨餈𩜴𥻓粢䭣薋𩆂䨏𩆃瓷❌薺疵玼骴髊𩨱茈𦶉訾 ᄍᆞᆼ〮 ˦ 字牸孶自漬眥眦骴髊㱴餗 |- | 5:011 || ㅅ || ᄉᆞᆼ ˧ 私思罳䰄偲愢緦颸絲司伺覗𥄶斯撕澌凘𪆁廝㒋虒傂禠㴲磃𩆵菥析徙師獅𤜳篩釃𦌿灑廝襹褷籭簛簁蓰 ᄉᆞᆼ〯 ˧˦ 徙璽死枲葸𤟧諰鰓禗躧蹝屣釃纚縰灑洒矖𥊂𩌦𩎉蓰簁籭漇史駛使 ᄉᆞᆼ〮 ˦ 賜錫瀃澌𣩠杫𣘩㮐四泗柶駟肆𩬶肄笥伺覗司僿思𩢲駛使𩌦屣灑洒曬釃䚕 |- | 5:013 || ㅆ || ᄊᆞᆼ ˧ 詞祠柌辭辤漦 ᄊᆞᆼ〯 ˧˦ 兕𧰽𠒃似仏姒㚶耜梩𦓨杞䎣枱𨐠祀𥘰𧝀汜巳士仕𣐈戺俟竢𥏳𢓪䇃𢈟𢉡逘𣀼𡱢涘騃 ᄊᆞᆼ〮 ˦ 寺嗣飤飼食飴事 |} === ㅣ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:014 || ㅋ || 킹〯 ˧˦ 企跂𠈮 킹〮 ˦ 棄企❌跂蚑 |- | 5:015 || ㄲ || 낑 ˧ 祇軝❌軙忯疧疷伎跂枝蚑𨙸岐歧祁耆鰭愭鬐 |- | 5:015 || ㄷ || 딩 ˧ 知𥎿䝷蜘𧐉鼅胝疷䟡躓𦙁 딩〯 ˧˦ 徵黹𧝉䌤緻絺希鵗 딩〮 ˦ 致致輊輖𦥎䡹質劕躓懫懥驇疐𨇈嚔❌𢷟智知置 |- | 5:016 || ㅌ || 팅 ˧ 摛攡螭彲離𡖟魑离𣉽絺瓻癡𢣕𠈴笞抬𪗪呞 팅〯 ˧˦ 褫搋拸䚦扡扯恥誀祉 팅〮 ˦ 眙 |- | 5:017 || ㄸ || 띵 ˧ 馳䮈池篪竾筂褫簃𠗺謻趨邸踟踶墀𡎰遲遟𨒈赿❌䜄坻汣𡊆沶泜蚳𧐏貾治耛菭持蛇 띵〯 ˧˦ 豸𧋈褫傂廌阤陁陊杝雉鴙鶨薙埃垁滍泜踶峙𠍰跱畤庤𤲵痔偫崻 띵〮 ˦ 地坔埊緻致穉稚䆈䄺䕌謘遲遟治値直植置褫彘 |- | 5:019 || ㄴ || 닝 ˧ 尼㞾怩跜秜旎 닝〯 ˧˦ 柅旎狔 닝〮 ˦ 膩𦡸 |- | 5:020 || ㅂ || 빙 ˧ 卑庳箄裨陴埤鞞錍椑陂波詖羆❌❌罷藣❌❌碑悲非餥裴蜚誹緋扉飛騛 빙〯 ˧˦ 俾卑髀脾鞞箄匕比妣䃾❌秕粃❌朼枇彼佊鄙啚否匪篚榧棐蜚奜蜰 빙〮 ˦ 臂嬖辟畀❌痹比庛芘賁跛詖陂披佊貱波藣祕秘泌柲鉍䪐閟毖鄪費❌粊轡䩛沸髴❌誹非芾茀 |- | 5:022 || ㅍ || 핑 ˧ 紕❌❌悂鈹鉟披㱟㨢被旇翍耚❌丕㔻伾秠怌駓䮆豾狉銔批霏䬠菲騑匪❌馡裶裴妃婓❌ 핑〯 ˧˦ 庀比庇疕仳諀吡批披㱟噽秠❌斐菲誹非悱朏 핑〮 ˦ 譬辟濞淠帔被襬費 |- | 5:024 || ㅃ || 삥 ˧ 毗紕辟𨈚❌❌阰❌魮❌鈚蚍螕琵批比膍肶㮰貔豼❌❌陴埤脾裨崥皮疲罷郫邳岯坯❌魾肥淝腓痱❌厞婓賁 삥〯 ˧˦ 婢埤䠋卑庳埤否痞岯圮䤏被骳罷陫❌ 삥〮 ˦ 鼻比紕枇𢈷庳卑避辟髲被鞁骳備俻犕糒奰贔屝厞陫❌痱腓菲蜚翡䠊剕❌費狒𥜿𥝃𦦻𤲳昲𪎰黂䆊 |- | 5:027 || ㅁ || 밍 ˧ 彌弥𢏏㜷𡝠瀰冞𥹐糜𩞁𩞇𪎭穈𪐎縻𥿫𦄐䌕靡㸏𤓒𦗕劘𪎕𠞧醾𨣿醿蘼蘪攠❌眉嵋湄𣽪𤃰攗楣郿媚麋麛黴微𧗬薇㵟溦霺 밍〯 ˧˦ 弭渳彌瀰𣴱沔敉侎𢘺芉美渼媺靡𦗕尾亹斖娓 밍〮 ˦ 寐媢媚郿篃魅靡未味 |- | 5:028 || ㅈ || 징 ˧ 支枝肢𨈪𨈙跂䧴鳷巵觶𧣨梔氏祇祗衹褆㩼多秖榰禔疧脂祗砥厎泜之芝 징〯 ˧˦ 紙帋坁汦汣❌抵觗㧗抵砥只𠮡軹枳咫𦐖❌疻旨𣅀指𢫾脂恉厎底𦒿止芷茝沚洔阯址趾畤 징〮 ˦ 寘忮伎觶𧣨觗至摯贄質鷙𪁊礩志誌識痣䏯織綕𥿮幟𢂴笫 |- | 5:031 || ㅊ || 칭 ˧ 蚩嗤𢾫媸鴟鵄眵 칭〯 ˧˦ 侈奓鉹𨪐誃哆恀䏧姼𡚼㶴袳移𧛧袲㢋㢁懘齒茝䊼 칭〮 ˦ 熾𧹹幟織旘𢂴志饎糦喜𩛉𩜮𩜂埴𡑌戠 |- | 5:032 || ㅅ || 싱 ˧ 施葹鍦鉇𥍸𥍢絁䌤𦂛䙾𧠜𧠉翅尸㕧鳲𨾋䲩屍蓍詩邿 싱〯 ˧˦ 弛𢐋㢮阤施豕矢笑屎始 싱〮 ˦ 翅𦐊翄翨𦑧施鍦釶啻翅試式弑殺煞𢎍識幟織始失 |- | 5:033 || ㅆ || 씽 ˧ 茬茌匙㮛堤提𦑡禔媞鍉時塒鰣 씽〯 ˧˦ 是諟惿媞𨸝䟗氏視市恃㤄𦧇舐𦧧咶狧 씽〮 ˦ 示謚𧨦豉嗜耆𩝙𨢍𦞯呩視侍寺䦙蒔 |- | 5:034 || ㆆ || ᅙᅵᆼ ˧ 伊洢咿吚黟黝❌ ᅙᅵᆼ〮 ˦ 縊 |- | 5:035 || ㅎ || 힝 ˧ 㕧屎䐖❌❌❌ |- | 5:035 || ㅇ || 잉 ˧ 移拖❌䔟簃❌扅❌栘鉹袲椸箷䈕㮛❌暆貤酏匜詑訑施❌❌蛇夷尼❌痍荑恞𢓡跠峓鐵侇桋䧅姨洟❌胰寅夤彛飴❌❌❌䬮怡眙詒❌貽瓵怠台頤宧圯異 잉〯 ˧˦ 以已苢苡酏祂肔胣匜迆迤施崺❌ 잉〮 ˦ 易㑥敡施袘❌緆衪貤❌移肄肆勩❌廙異異食詒貽 |- | 5:037 || ㄹ || 링 ˧ 離蘺籬杝㰚㒿䍦❌灕漓離攡摛褵縭璃䅻醨离鸝鵹❌❌❌矖纚驪孋穲䕻❌麗罹蠡盠❌劙❌棃❌棃梨犂黧❌犂❌❌犁蔾❌菞釐剺❌氂犛狸剺嫠來倈徠䅘貍狸❌猍梩❌ 링〯 ˧˦ 里理鯉❌俚悝裏李邐麗履 링〮 ˦ 詈茘離离利莅蒞位吏 |- | 5:040 || ㅿ || ᅀᅵᆼ ˧ 兒唲而胹臑腝❌❌洏聏陑栭鮞髵耏耐鴯怠❌‗ 濡 ᅀᅵᆼ〯 ˧˦ 爾爾爾邇邇邇耳耳珥駬䋙餌柅鑈檷 ᅀᅵᆼ〮 ˦ 二貳㒃樲餌㢽❌珥鉺衈咡刵毦髶聏 |} === ㆎ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:041 || ㄷ || ᄃᆡᆼ〮 ˦ 戴載襶 |- | 5:041 || ㅌ || ᄐᆡᆼ ˧ 胎孡台能駘鮐邰斄釐漦 ᄐᆡᆼ〮 ˦ 貸態詒紿怠䈚箈殆待 |- | 5:041 || ㄸ || ᄄᆡᆼ ˧ 臺𡌬薹籉儓擡鮐駘跆苔炱炲䈚箈 ᄄᆡᆼ〯 ˧˦ 待𥩳逮迨隶𨽿駘紿詒殆怠䈚箈靆 ᄄᆡᆼ〮 ˦ 代岱袋黛逮曃靆棣埭𥓏𡍖瑇❌毒玳 |- | 5:043 || ㅂ || ᄇᆡᆼ ˧ 杯盃𦈶𦈧𠤯桮 ᄇᆡᆼ〮 ˦ 背輩軰 |- | 5:043 || ㅍ || ᄑᆡᆼ ˧ 胚妚衃阫坏培醅❌ ᄑᆡᆼ〯 ˧˦ 朏 ᄑᆡᆼ〮 ˦ 配妃朏 |- | 5:043 || ㅃ || ᄈᆡᆼ ˧ 裴徘俳培陪倍阫 ᄈᆡᆼ〯 ˧˦ 琲㻗痱❌倍 ᄈᆡᆼ〮 ˦ 佩佩背偝負倍㔨北邶鄁倍焙❌孛茀勃誖悖哱怫㫲琲㻗拔萯 |- | 5:044 || ㅁ || ᄆᆡᆼ ˧ 枚玫梅楳某槑❌鋂脢脄❌膴酶䊈莓每䍙禖媒煤塺 ᄆᆡᆼ〯 ˧˦ 浼每痗脢脄 ᄆᆡᆼ〮 ˦ 妹昧㫚眛沬韎❌每痗脢脄沕琩❌冒媒 |- | 5:046 || ㅈ || ᄌᆡᆼ ˧ 哉裁栽𦳦載災灾菑甾𤆄 ᄌᆡᆼ〯 ˧˦ 宰䏁縡載 ᄌᆡᆼ〮 ˦ 再載縡 |- | 5:046 || ㅊ || ᄎᆡᆼ ˧ 猜偲 ᄎᆡᆼ〯 ˧˦ 采採寀彩棌綵䰂茝 ᄎᆡᆼ〮 ˦ 菜寀采縩 |- | 5:046 || ㅉ || ᄍᆡᆼ ˧ 裁才材財鼒纔 ᄍᆡᆼ〯 ˧˦ 在 ᄍᆡᆼ〮 ˦ 在裁栽載酨 |- | 5:047 || ㅅ || ᄉᆡᆼ ˧ 鰓䚡䰄思罳❌顋毸 ᄉᆡᆼ〮 ˦ 塞僿簺賽 |- | 5:047 || ㆆ || ᅙᆡᆼ ˧ 哀埃㶼欸唉 ᅙᆡᆼ〯 ˧˦ 欸唉款靉靄藹娭毐 ᅙᆡᆼ〮 ˦ 愛靉曖僾薆藹靄欸 |- | 5:048 || ㅎ || ᄒᆡᆼ ˧ 咍 ᄒᆡᆼ〯 ˧˦ 海❌醢❌ |- | 5:048 || ㆅ || ᅘᆡᆼ ˧ 孩㜾❌頦 ᅘᆡᆼ〯 ˧˦ 亥侅劾 ᅘᆡᆼ〮 ˦ 瀣劾 |- | 5:048 || ㄹ || ᄅᆡᆼ ˧ 來逨❌倈萊徠䅘❌騋淶崍 ᄅᆡᆼ〮 ˦ 徠倈來睞賚萊 |} === ㅢ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:049 || ㄱ || 긩 ˧ 羈䩭鞿羇踦奇倚踦奇畸觭掎剞飢饑❌肌❌姬箕❌❌箕諆❌錤棋檱踑其基朞期祺居機鐖璣禨嘰饑鞿磯譏耭蟣幾刏 긩〯 ˧˦ 己紀几機㲹麂掎踦剞❌庋攱庪㨳蟣❌穖幾 긩〮 ˦ 寄徛❌冀兾驥❌懻❌穊❌穖覬幾洎記其忌已㤅近旣漑曁 |- | 5:051 || ㅋ || 킝 ˧ 敧崎觭踦䗁徛碕榿欺倛僛❌娸❌❌❌ 킝〯 ˧˦ 綺觭起杞梩❌㞯玘芑豈䔇 킝〮 ˦ 器❌器亟氣❌乞❌ |- | 5:052 || ㄲ || 끵 ˧ 奇騎錡琦碕埼隑其萁稘期琪淇祺麒騏鶀❌旗棊碁櫀蜝綦綨❌❌𤪌璂蘄祈❌蘄肵旂頎畿圻幾❌刏❌機碕 끵〯 ˧˦ 技伎妓䗁錡跽❌ 끵〮 ˦ 芰茤騎䗁妓技❌洎垍曁❌墍蔇忌鵋誋惎㥍諅綦禨璣幾曁刏 |- | 5:054 || ㆁ || ᅌᅴᆼ ˧ 宜義儀❌檥轙議鸃䴊㕒嶬涯崖疑儗嶷觺沂凒皚溰 ᅌᅴᆼ〯 ˧˦ 蟻蟻蛾❌❌轙艤檥礒硪齮錡擬譺懝儗疑薿❌孴❌矣顗 ᅌᅴᆼ〮 ˦ 義誼❌竩議儗❌毅藙 |- | 5:055 || ㅈ || 즹 ˧ 菑載𤰭䅔𦿨淄甾緇純䊷輜椔錙鶅𨿴甾 즹〯 ˧˦ 滓胏𦞤𦙰笫緇 즹〮 ˦ 胾椔緇菑倳事輜剚𨧫 |- | 5:056 || ㅊ || 칑 ˧ 差嵯嵳䡨𨍃 칑〮 ˦ 廁厠 |- | 5:056 || ㆆ || ᅙᅴᆼ ˧ 漪猗䝝兮椅欹旖禕醫毉噫意億懿嘻譆❌衣依譩 ᅙᅴᆼ〯 ˧˦ 倚奇椅檹輢猗旖㫊䭲譩醫醷臆旖㕈庡依偯 ᅙᅴᆼ〮 ˦ 意薏倚猗輢懿饐❌饖撎衣䵝 |- | 5:058 || ㅎ || 힁 ˧ 犧曦㬢爔羲戲嚱❌❌❌❌檥巇嶬桸獻僖嬉嘻禧釐譆熹暿熺熙娭㜯稀俙晞睎豨狶欷唏鵗希 힁〯 ˧˦ 喜憘憙嬉豨唏俙霼 힁〮 ˦ 戲齂哂嚊屭豷燹咥憙喜憘嬉欷唏愾餼䊠旣熂霼墍❌❌摡❌黖 |} === ㅚ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:060 || ㄱ || 굉 ˧ 傀❌❌❌❌瑰瓌璝 굉〮 ˦ 儈會襘檜澮膾鱠獪䯤鬠❌旝鄶劊會廥憒慖幗蔮刏 |- | 5:061 || ㅋ || 쾽 ˧ 恢❌詼盔魁❌悝 쾽〮 ˦ ❌塊蕢蒯❌堁 |- | 5:061 || ㆁ || ᅌᅬᆼ ˧ 嵬峞隗阢桅磑 ᅌᅬᆼ〯 ˧˦ 隗嵬峞䫥頠 ᅌᅬᆼ〮 ˦ 外磑 |- | 5:061 || ㄷ || 됭 ˧ 鎚追❌頧搥槌磓堆❌塠㕍❌❌敦 됭〮 ˦ 祋杸對轛❌碓敦 |- | 5:062 || ㅌ || 툉 ˧ 推蓷焞 툉〯 ˧˦ 腿 툉〮 ˦ 娧脫稅駾蛻兌退𨑧𢓇 |- | 5:063 || ㄸ || 뙹 ˧ 隤頹墤穨尵僓魋 뙹〯 ˧˦ 鐓憝隊 뙹〮 ˦ 兌𩊭銳𠜑奪隊䨴𩅥𩄮薱憝譈懟憞譵鐓駾 |- | 5:063 || ㄴ || 뇡 ˧ 挼捼 뇡〯 ˧˦ 餧餒鯘鮾脮腇腇 뇡〮 ˦ 內 |- | 5:064 || ㅈ || 죙〮 ˦ 最蕞棷粹晬綷❌祽 |- | 5:064 || ㅊ || 쵱 ˧ 崔催縗衰䙑 쵱〯 ˧˦ 漼璀皠洒綷萃 쵱〮 ˦ 襊㝮竄倅萃淬焠𠗚啐啛 |- | 5:065 || ㅉ || 쬥 ˧ 摧漼凗崔㠑磪𡹐 쬥〯 ˧˦ 辠𡽕 쬥〮 ˦ 蕞 |- | 5:065 || ㅅ || 쇵 ˧ 𣯧𦸏挼䪎鞖 쇵〮 ˦ 碎粹誶 |- | 5:065 || ㆆ || ᅙᅬᆼ ˧ 隈❌椳煨䋿偎畏 ᅙᅬᆼ〯 ˧˦ 猥❌椳萎 ᅙᅬᆼ〮 ˦ 薈懀濊 |- | 5:065 || ㅎ || 횡 ˧ 灰❌䝇豗拻 횡〯 ˧˦ 賄悔 횡〮 ˦ ❌噦鐬翽誨晦悔❌靧頮 |- | 5:066 || ㆅ || ᅘᅬᆼ ˧ 回廻茴洄徊瑰槐瘣 ᅘᅬᆼ〯 ˧˦ 瘣壞廆匯滙回 ᅘᅬᆼ〮 ˦ 會禬繪䙡璯潰繪繪䜋聵瞶闠回匯䔇 |- | 5:067 || ㄹ || 룅 ˧ 雷䨓❌罍❌鑘𨯔❌礧轠瓃❌ 룅〯 ˧˦ 磊磥礌礧❌㠥礨壘櫑儡❌❌❌㒦❌蕾 룅〮 ˦ 酹類耒礧壘礌雷㵢攂 |} === ㅐ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:068 || ㄱ || 갱 ˧ 佳街皆偕湝階堦楷喈❌荄痎䕸稭祴該垓畡陔閡絯胲侅晐峐剴祴裓 갱〯 ˧˦ 解改胲❌陔閡 갱〮 ˦ 蓋蓋匃匄丐懈解繲廨解戒誡悈介界堺❌魪玠琾㠹价尬芥疥蚧❌䯰❌屆漑摡槪扢槪㧉剴 |- | 5:070 || ㅋ || 캥 ˧ 揩❌❌❌開 캥〯 ˧˦ 鍇楷愷凱豈鎧塏闓 캥〮 ˦ 磕礚愒❌渴嘅❌揩慨鎧闓欬咳愾 |- | 5:070 || ㆁ || ᅌᅢᆼ ˧ 厓崖涯漄睚倪皚❌凒敱 ᅌᅢᆼ〯 ˧˦ 騃 ᅌᅢᆼ〮 ˦ 艾❌㣻礙硋㝵儗閡 |- | 5:071 || ㄷ || 댕〮 ˦ 帶㿃蔕蹛遞 |- | 5:071 || ㅌ || 탱〮 ˦ 泰太太大忕㥭汰汏蠆𧀱蔕慸 |- | 5:071 || ㄸ || 땡〯 ˧˦ 廌豸豸 땡〮 ˦ 大軑釱汏 |- | 5:072 || ㄴ || 냉 ˧ 能 냉〯 ˧˦ 嬭㛋㚷乃迺鼐 냉〮 ˦ 柰耐𦓎能𣉘奈鼐 |- | 5:072 || ㅂ || 뱅〯 ˧˦ 擺❌捭 뱅〮 ˦ 貝狽伂沛拜扒敗 |- | 5:072 || ㅍ || 팽〮 ˦ 霈沛肺浿派湃㵒 |- | 5:073 || ㅃ || 뺑 ˧ 牌❌❌蠯螷❌排俳 뺑〯 ˧˦ 罷 뺑〮 ˦ 旆沛茷❌柭軷粺❌稗❌❌韛❌❌排糒敗唄 |- | 5:073 || ㅁ || 맹 ˧ 埋貍霾䨪 맹〯 ˧˦ 買 맹〮 ˦ 眛昧沬賣韎霾邁❌勱佅 |- | 5:074 || ㅈ || 쟁 ˧ 齋齊𩱳 쟁〯 ˧˦ 跐 쟁〮 ˦ 債責瘵祭 |- | 5:074 || ㅊ || 챙 ˧ 釵靫扠搋差䡨 챙〮 ˦ 蔡䌨縩瘥差衩𣲾 |- | 5:075 || ㅉ || 쨍 ˧ 柴祡豺犲儕 쨍〮 ˦ 眦❌疵眥砦柴 |- | 5:075 || ㅅ || 생 ˧ 簁篩 생〯 ˧˦ 灑洒躧蹝纚徙 생〮 ˦ 曬灑洒鎩𨦅殺閷煞 |- | 5:076 || ㆆ || ᅙᅢᆼ ˧ 娃哇❌挨唲 ᅙᅢᆼ〯 ˧˦ 矮㾨躷 ᅙᅢᆼ〮 ˦ 藹靄䨠馤壒❌濭隘阨搤噫❌呃嗄餲喝 |- | 5:076 || ㆅ || ᅘᅢᆼ ˧ 諧湝骸膎鞵鞋鮭 ᅘᅢᆼ〯 ˧˦ 蟹蟹獬❌鮭嶰澥解駭絯豥駴夥 ᅘᅢᆼ〮 ˦ 害妎邂解械❌薤❌瀣齘 |- | 5:077 || ㄹ || 랭〮 ˦ 賴瀨籟藾癩襰厲糲蠣賚 |} === ㅙ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:077 || ㄱ || 괭 ˧ 乖❌媧緺騧❌蝸 괭〯 ˧˦ 掛 괭〮 ˦ 卦掛挂絓罣詿怪怪壞夬澮獪㹟狤 |- | 5:078 || ㅋ || 쾡 ˧ ❌華䦱 쾡〮 ˦ 蒯❌蕢塊喟嘳㕟快噲 |- | 5:078 || ㆁ || ᅌᅫᆼ〮 ˦ 聵❌❌ |- | 5:078 || ㅊ || 쵕〮 ˦ 嘬 |- | 5:078 || ㆆ || ᅙᅫᆼ ˧ 蛙鼃洼 |- | 5:078 || ㆅ || ᅘᅫᆼ ˧ 懷櫰槐褢❌❌淮 ᅘᅫᆼ〮 ˦ 壞畵罫繣澅絓話❌躗吳 |} === ㅟ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:079 || ㄱ || 귕 ˧ 嬀潙佹龜騩歸 귕〯 ˧˦ 軌軌軌匭氿漸宄❌晷簋詭垝陒佹❌祪䤥庪庋攱傀鬼 귕〮 ˦ 媿愧❌聭騩攰庪貴瞶劌劂❌撅橛蹶❌❌鱖鱥 |- | 5:080 || ㅋ || 큉 ˧ 虧𧇾𩏣蘬巋 큉〯 ˧˦ 巋蘬 큉〮 ˦ 喟嘳㕟䙡巋缺 |- | 5:081 || ㄲ || 뀡 ˧ 逵㙺夔犪騤戣鍨頯頄馗 뀡〯 ˧˦ 跪 뀡〮 ˦ 匱鐀櫃饋歸蕢䕚簣籄㙺 |- | 5:081 || ㆁ || ᅌᅱᆼ ˧ 危峗峞帷爲韋幃褘湋違闈圍巍嵬犩❌ ᅌᅱᆼ〯 ˧˦ 頠姽峗瓦韙偉瑋煒颹暐韡❌葦 ᅌᅱᆼ〮 ˦ 僞胃❌謂渭蝟猬❌㥜媦緯圍彙❌❌魏犩❌衛熭❌❌轊䡺❌❌彘㻰 |- | 5:082 || ㄷ || 뒹〮 ˦ 轛 |- | 5:082 || ㆆ || ᅙᅱᆼ ˧ 逶倭䴧蜲痿萎委威葳蝛 ᅙᅱᆼ〯 ˧˦ 委骩磈嵬 ᅙᅱᆼ〮 ˦ 餧❌委骩尉慰蔚霨罻畏威 |- | 5:083 || ㅎ || 휭 ˧ 麾戲摩撝暉煇煒輝揮楎椲翬❌褘徽幑㫎 휭〯 ˧˦ 毁毁燬䅏❌毇❌烜䃣蟲虺蘬❌❌燬卉 휭〮 ˦ 毁諱卉 |- | 5:084 || ㅇ || 윙〯 ˧˦ 蔿蘤䦱❌❌ 윙〮 ˦ 位爲 |} === ㆌ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:084 || ㄱ || ᄀᆔᆼ ˧ 規槼摫瞡❌雉鳺 ᄀᆔᆼ〯 ˧˦ 癸湀 ᄀᆔᆼ〮 ˦ 季 |- | 5:084 || ㅋ || ᄏᆔᆼ ˧ 闚窺 ᄏᆔᆼ〯 ˧˦ 跬頃蹞窺❌頍頯 |- | 5:085 || ㄲ || ᄁᆔᆼ ˧ 葵楑鄈 ᄁᆔᆼ〯 ˧˦ 揆 ᄁᆔᆼ〮 ˦ 悸 |- | 5:085 || ㄷ || ᄃᆔᆼ ˧ 腄追䨨 |- | 5:085 || ㄸ || ᄄᆔᆼ ˧ 椎桘鎚錘槌甀魋椎鬌 ᄄᆔᆼ〮 ˦ 縋䋘槌膇㾽硾倕磓墜錘腄甀墜隊隧❌懟譵 |- | 5:086 || ㄴ || ᄂᆔᆼ〮 ˦ 諉 |- | 5:086 || ㅈ || ᄌᆔᆼ ˧ 劑檇㰎崔墔隹崒❌厜隹萑鵻騅錐 ᄌᆔᆼ〯 ˧˦ 觜❌嘴❌嶉捶棰錘箠菙 ᄌᆔᆼ〮 ˦ 醉檇雋惴諈錘 |- | 5:087 || ㅊ || ᄎᆔᆼ ˧ 吹龡炊推蓷衰 ᄎᆔᆼ〯 ˧˦ 趡揣 ᄎᆔᆼ〮 ˦ 翠臎吹䶴龡出 |- | 5:087 || ㅉ || ᄍᆔᆼ〮 ˦ 萃瘁悴顇踤 |- | 5:087 || ㅅ || ᄉᆔᆼ ˧ 綏浽荽荾䒘雖睢濉衰榱 ᄉᆔᆼ〯 ˧˦ 髓髓❌䯝䯝❌膸瀡靃❌巂嶲水準 ᄉᆔᆼ〮 ˦ 邃粹睟誶祟帥帨率 |- | 5:087 || ㅆ || ᄊᆔᆼ ˧ 隨隋遺垂陲倕❌腄誰唯譙脽 ᄊᆔᆼ〯 ˧˦ 菙❌ ᄊᆔᆼ〮 ˦ 遂燧❌鐩澻㴚襚❌璲繸❌❌隧❌隊旞❌❌彗篲穗❌穟瑞睡倕 |- | 5:089 || ㆆ || ᅙᆔᆼ〮 ˦ 恚烓 |- | 5:089 || ㅎ || ᄒᆔᆼ ˧ 墮❌❌隳隋綏觽鐫鐫睢倠 ᄒᆔᆼ〮 ˦ 睢隋綏墮挼侐 |- | 5:090 || ㅇ || ᄋᆔᆼ ˧ 惟維唯濰遺壝蠵❌❌觿 ᄋᆔᆼ〯 ˧˦ 洧鮪痏䔺❌芛芟唯踓趡壝 ᄋᆔᆼ〮 ˦ 遺❌壝瞶蜼 |- | 5:090 || ㄹ || ᄅᆔᆼ ˧ 羸纍縲累欙樏㹎❌虆藟❌蘲蔂絫 ᄅᆔᆼ〯 ˧˦ 壘藟虆蘽櫐讄❌❌❌礧❌㠥❌礨礨❌蠝❌累樏誄蜼猚 ᄅᆔᆼ〮 ˦ 類禷蘱率淚累絫 |- | 5:092 || ㅿ || ᅀᆔᆼ ˧ 甤蕤苼痿緌綏䅑䅗桵㮃 ᅀᆔᆼ〯 ˧˦ 繠橤蘂❌蕊 |} === ㅖ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:008 || ㄱ || 곙 ˧ 雞稽𮃛笄䈕枅𣓖卟乩 곙〮 ˦ 罽𦇧𣯅𦆡𣽄猘狾計繼㡭繫毄係❌髻結𨜒薊 |- | 6:008 || ㅋ || 콍 ˧ 谿溪磎嵠鸂䳶 콍〯 ˧˦ 啓棨𢧊綮启稽 콍〮 ˦ 愒憇𢠾偈❌揭䔾甈契栔鍥㓶挈 |- | 6:009 || ㄲ || 꼥〮 ˦ 偈碣嵑 |- | 6:009 || ㆁ || ᅌᅨᆼ〯 ˧˦ 掜 ᅌᅨᆼ〮 ˦ ❌甈乂刈㣻艾 |- | 6:010 || ㄷ || 뎽 ˧ 氐互低仾詆呧柢羝牴隄䧑堤鞮䩚磾 뎽〯 ˧˦ 邸䣌柢詆呧阺坻弤抵掋牴觝軝疧疷底㡳氐提堤底 뎽〮 ˦ 帝諦諟𧫚締蔕䗖柢泜氐疐嚔㗣 |- | 6:011 || ㅌ || 톙 ˧ 梯鷈鷉鵜𩀗 톙〯 ˧˦ 體軆躰涕緹紙醍 톙〮 ˦ 替暜朁涕洟鬀鬄剃殢揥楴𧝐褅裼薙雉𡲕傺袲 |- | 6:012 || ㄸ || 똉 ˧ 題提媞偍姼禔褆緹鞮睼瑅騠踶鶗嗁啼諦㖒㖷渧蹏蹄締綈䬾稊𦯔稺䄺𧀾銻𧋘鵜鴺罤䨑荑梯鮧鯷桋鴺㡗折 똉〯 ˧˦ 弟悌娣遞递𮞏 똉〮 ˦ 第弟悌娣睇眱珶遞迭递𮞏遰褅締杕軑釱踶提題鬄髢錫鶗鷤逮棣滯㿃䐭彘 |- | 6:014 || ㄴ || 녱 ˧ 泥埿臡 녱〯 ˧˦ 禰祢檷鑈鈮濔瀰薾爾泥 녱〮 ˦ 泥 |- | 6:015 || ㅂ || 볭 ˧ 篦豍❌㡙鎞❌狴❌ 볭〮 ˦ 閉箄嬖蔽草弊弊鷩鄨廢癈祓苃茀 |- | 6:015 || ㅍ || 폥 ˧ ❌批錍鈚漂 폥〮 ˦ 媲睤❌䁹辟壀埤僻濞淠潎❌肺杮 |- | 6:016 || ㅃ || 뼹 ˧ 鼙鞞𧯿椑㼰膍 뼹〯 ˧˦ 陛陛梐狴陛髀䯗䏶❌脾肶 뼹〮 ˦ 弊獘斃弊幣❌❌薜蘗❌吠犻❌茷 |- | 6:017 || ㅁ || 몡 ˧ 迷麛麑❌❌ 몡〯 ˧˦ 米眯䋛❌洣 몡〮 ˦ 袂 |- | 6:017 || ㅈ || 졩 ˧ 齎賷韲齏𦦏虀懠𢥎擠躋隮齊𨹷𢁃 졩〯 ˧˦ 濟泲㧗 졩〮 ˦ 霽擠濟𩯶祭際穄制製䱥鰶㫼晣晢淛浙折 |- | 6:018 || ㅊ || 촁 ˧ 妻萋霋凄淒悽緀 촁〯 ˧˦ 泚玼萋緀 촁〮 ˦ 切砌墄𥉻妻掣𢳅摯觢挈懘滯慸 |- | 6:018 || ㅉ || 쪵 ˧ 齊臍蠐 쪵〯 ˧˦ 薺齊癠鮆鱭 쪵〮 ˦ 嚌懠穧𨣧劑齊薺癠齌眥眦 |- | 6:019 || ㅅ || 솅 ˧ 西栖棲犀屖遟凘澌嘶撕 솅〯 ˧˦ 洗灑 솅〮 ˦ 細壻婿栖世貰勢埶殺閷 |- | 6:020 || ㅆ || 쏑 ˧ 栘 쏑〮 ˦ 誓逝遰遞筮簭噬澨遾忕 |- | 6:020 || ㆆ || ᅙᅨᆼ ˧ 鷖繄䃜瑿㙠黳 ᅙᅨᆼ〮 ˦ 瘞❌餲医瞖繄翳蘙嫕瘱曀㙪㙠殪縊 |- | 6:020 || ㅎ || 혱 ˧ 醯❌䤈橀 |- | 6:021 || ㆅ || ᅘᅨᆼ ˧ 兮❌奚㜎傒蹊❌騱貕豀謑鼷嵇 ᅘᅨᆼ〯 ˧˦ 徯諐蹊❌傒謑謑奚 ᅘᅨᆼ〮 ˦ 系繫係結禊妎盻 |- | 6:021 || ㅇ || 옝 ˧ 倪齯鯢蜺䘽輗❌棿麑猊猊霓兒 옝〮 ˦ 曳洩❌袣❌泄呭詍㖂䛖枻栧抴跇裔❌㵝㵩㳿㵩勩詣栺睨倪堄帠羿❌寱藝蓺❌槸褹袂囈 |- | 6:022 || ㄹ || 롕 ˧ 黎❌❌犁黧藜蔾瓈邌梨驪鸝蠡 롕〯 ˧˦ 禮澧醴鱧蠡䗍劙盠欐 롕〮 ˦ 麗䕻儷儷❌欐攦戾唳淚綟棙悷❌蜧茘❌蛠❌❌隷隷劙蠫❌㓯盭沴離例列栵迾裂厲厲礪❌❌糲爄勵蠣❌癘❌㾐❌ |} === ㆋ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:024 || ㄱ || ᄀᆒᆼ ˧ 圭窐❌❌❌閨袿邽蠲 ᄀᆒᆼ〮 ˦ 桂筀䳏 |- | 6:024 || ㅋ || ᄏᆒᆼ ˧ 睽聧奎暌刲㨒 |- | 6:025 || ㄷ || ᄃᆒᆼ〮 ˦ 綴餟醊腏畷錣 |- | 6:025 || ㅈ || ᄌᆒᆼ〮 ˦ 蕝蕞贅 |- | 6:025 || ㅊ || ᄎᆒᆼ〮 ˦ 脃膬毳橇竁喙 |- | 6:025 || ㅅ || ᄉᆒᆼ〮 ˦ 歲繐❌稅帨帥蛻涗裞䬽鐫說 |- | 6:025 || ㅆ || ᄊᆒᆼ〮 ˦ 篲 |- | 6:025 || ㆆ || ᅙᆒᆼ ˧ 烓❌ ᅙᆒᆼ〮 ˦ 穢薉濊獩 |- | 6:026 || ㅎ || ᄒᆒᆼ〮 ˦ 嘒嚖❌暳噦喙𣨶𤸁𠿔顪噦翽 |- | 6:026 || ㆅ || ᅘᆒᆼ ˧ 攜㩗携觿鑴蠵酅❌巂畦 ᅘᆒᆼ〮 ˦ 慧轊惠蕙譓憓 |- | 6:026 || ㅇ || ᄋᆒᆼ〮 ˦ 叡睿銳梲 |- | 6:027 || ㅿ || ᅀᆒᆼ〮 ˦ 汭內枘芮蜹 |} === ㅗ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:027 || ㄱ || 공 ˧ 孤❌觚❌柧軱呱❌箛䉉苽菰罛❌姑酤沽蛄鴣䧸辜❌❌橭❌盬夃 공〯 ˧˦ 古詁故估牯沽酤罟苦楛鼓❌瞽鼓股❌骰羖❌蠱監賈夃 공〮 ˦ 顧雇固凅錮鯝稒㧽痼故酤沽詁蠱 |- | 6:028 || ㅋ || 콩 ˧ 枯刳恗 콩〯 ˧˦ 苦䇢 콩〮 ˦ 庫袴絝胯跨❌ |- | 6:029 || ㆁ || ᅌᅩᆼ ˧ 吾浯梧鼯❌❌郚吳㻍珸鋘鋙 ᅌᅩᆼ〯 ˧˦ 五伍午迕仵旿 ᅌᅩᆼ〮 ˦ 誤悞悟晤旿晤捂梧忤牾午蘁逜寤❌遻❌迕俉愕 |- | 6:029 || ㄷ || 동 ˧ 都闍堵 동〯 ˧˦ 覩堵❌陼暏賭❌楮肚士 동〮 ˦ 妒妬奼㓃宅詫秅秺蠧螙❌斁❌睪 |- | 6:030 || ㅌ || 통 ˧ 稌 통〯 ˧˦ 土吐稌 통〮 ˦ 兎鵵菟吐 |- | 6:031 || ㄸ || 똥 ˧ 徒途墿峹𡷣荼𣘻余涂捈駼鵌鷋䅷䔑塗屠瘏菟𧈋檡兎圖鍍 똥〯 ˧˦ 杜荰土赭𢾅剫 똥〮 ˦ 度渡𣳥鍍塗 |- | 6:031 || ㄴ || 농 ˧ 奴㚢仅孥帑駑笯砮 농〯 ˧˦ 怒弩砮努 농〮 ˦ 笯怒㣽𢘂 |- | 6:032 || ㅂ || 봉 ˧ 逋餔哺誧晡 봉〯 ˧˦ 補䋠❌圃甫❌譜諩 봉〮 ˦ 布尃抪圃 |- | 6:032 || ㅍ || 퐁 ˧ 鋪痡鯆❌❌抪 퐁〯 ˧˦ 普浦誧溥 퐁〮 ˦ 怖鋪誧 |- | 6:033 || ㅃ || 뽕 ˧ 蒲莆蒱酺匍扶䒀瓿 뽕〯 ˧˦ 簿部蔀 뽕〮 ˦ 步❌荹捕哺餔䊇❌酺脯鞴 |- | 6:033 || ㅁ || 몽 ˧ 模橅❌❌謨謩嫫❌❌膜摹摸撫墓母 몽〯 ˧˦ 姥莽茻❌姆 몽〮 ˦ 暮慕募墓謨慔 |- | 6:034 || ㅈ || 종 ˧ 租蒩菹𦼬𧂚𧀽𦵔𦯓苴 종〯 ˧˦ 祖珇駔組阻岨俎柤𤕲齟詛 종〮 ˦ 作詛𥛜謯作 |- | 6:035 || ㅊ || 총 ˧ 麤麆粗觕初 총〯 ˧˦ 楚礎濋憷❌ 총〮 ˦ 措厝錯醋楚 |- | 6:035 || ㅉ || 쫑 ˧ 徂且鉏鋤助耡 쫑〯 ˧˦ 粗麆𧇿 쫑〮 ˦ 祚胙飵阼葄助耡鉏莇 |- | 6:036 || ㅅ || 송 ˧ 蘇穌㢝酥𦣑𨢭蔬疏疎𤕠梳𣐌𣓜疋綀釃斯 송〯 ˧˦ 所𢨷䝪糈 송〮 ˦ 素愫榡傃嗉訴𧪜愬㴑遡溯泝塑塐疏𥿇 |- | 6:037 || ㆆ || ᅙᅩᆼ ˧ 烏於杇圬釫❌汙汚於嗚惡 ᅙᅩᆼ〯 ˧˦ 塢䃖埡瑦鄔 ᅙᅩᆼ〮 ˦ 汙洿盓❌惡䛩䜑堊嗚 |- | 6:037 || ㅎ || 홍 ˧ 呼虖謼嘑滹❌淲❌❌泘膴幠芋 홍〯 ˧˦ 虎琥滸滹許 홍〮 ˦ 謼嘑呼戽 |- | 6:038 || ㆅ || ᅘᅩᆼ ˧ 胡❌𠴱箶❌湖瑚餬❌䭌醐❌糊粘❌❌❌䉿❌鶘乎虖壷弧狐❌瓠葫 ᅘᅩᆼ〯 ˧˦ 戶昈雇❌扈❌怙❌岵祜酤楛鄠❌下芐芦嫭嫮橭 ᅘᅩᆼ〮 ˦ 護濩頀䪝穫互枑冱瓠嫮嫭涸 |- | 6:039 || ㄹ || 롱 ˧ 盧蘆籚廬鑢爐櫨鱸壚艫轤瓐黸獹瀘矑纑顱髗❌❌玈❌❌ 롱〯 ˧˦ 魯櫓櫓㯭艣虜擄鹵滷塷瀂❌ 롱〮 ˦ 路璐露簬❌鷺❌鸕鴼潞輅賂 |} === ㅏ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:041 || ㄱ || 강 ˧ 歌謌❌戨❌牁哥柯鴚舸渮荷渮嘉佳加笳茄鴐駕珈耞跏迦痂枷葭麚豭猳家 강〯 ˧˦ 哿笴舸菏荷❌賈檟榎夏假徦嘏椵斝斝 강〮 ˦ 箇個介駕架榢枷稼嫁幏假叚下斝斝價賈 |- | 6:042 || ㅋ || 캉 ˧ 珂砢軻呿 캉〯 ˧˦ 可岢軻坷 캉〮 ˦ 軻坷髂䯊❌ |- | 6:043 || ㆁ || ᅌᅡᆼ ˧ 莪峨峨我娥俄哦睋蛾䖸鵝❌䳘鵞牙芽枒㧎衙涯厓 ᅌᅡᆼ〯 ˧˦ 我騀硪峨雅鴉庌牙疋 ᅌᅡᆼ〮 ˦ 餓訝迓御輅衙庌砑牙 |- | 6:044 || ㄷ || 당 ˧ 多多奓 당〯 ˧˦ 嚲癉哆打 당〮 ˦ 癉憚咤喥䖳㓃宅❌奼哆奓 |- | 6:044 || ㅌ || 탕 ˧ 佗他拕拖扡蛇詑訑 탕〯 ˧˦ 姹 탕〮 ˦ 詫❌咤侘差 |- | 6:045 || ㄸ || 땅 ˧ 駝駞它沱沲池陀岮陁阤跎❌佗紽酡鮀迤池鼉鱓鱣驒馱秅茶𣘻涂塗 땅〯 ˧˦ 拕拖扡佗柁舵䑨杕袉沱❌❌沲阤陀陊爹 땅〮 ˦ 馱他大 |- | 6:046 || ㄴ || 낭 ˧ 那難單儺拏挐笯 낭〯 ˧˦ 娜那袲橠難❌旎儺 낭〮 ˦ 柰那哪㖠 |- | 6:047 || ㅂ || 방 ˧ 波碆番僠嶓巴笆芭豝❌羓鈀 방〯 ˧˦ 跛❌簸播把 방〮 ˦ 播譒磻簸霸伯灞壩䃻靶弝杷欛 |- | 6:048 || ㅍ || 팡 ˧ 頗❌坡岥❌陂❌玻葩苩舥芭 팡〯 ˧˦ 頗叵 팡〮 ˦ 破帊帕袙怕❌❌ |- | 6:048 || ㅃ || 빵 ˧ 婆鄱番皤❌番杷爬把琶 빵〮 ˦ 縛杷䆉❌罷 |- | 6:049 || ㅁ || 망 ˧ 摩擵磨❌麼❌䯢魔劘攠麻菻蟆䗫蟇 망〯 ˧˦ 麼䯢馬 망〮 ˦ 磨禡貊伯榪罵䣕䣖鬕 |- | 6:049 || ㅈ || 장 ˧ 樝❌𣕈柤齟 장〯 ˧˦ 左𡯛鮓䱹𩽫苴苲 장〮 ˦ 左佐𡯛作詐醡笮苲榨𨣮𨢦溠咋 |- | 6:050 || ㅊ || 창 ˧ 蹉嵯磋瑳搓差叉杈靫扠差艖舣鎈 창〯 ˧˦ 瑳 창〮 ˦ 磋蹉差汊衩 |- | 6:051 || ㅉ || 짱 ˧ 醝鹺艖𦪸䑘㽨瘥𣩈嵳𥰭齹䣜鄼 |- | 6:051 || ㅅ || 상 ˧ 娑挱莎莏挲沙䤬𨪍桫髿𣯌犠獻戲傞𠈱沙𣲡砂紗𦀟鯊㲚髿硰 상〯 ˧˦ 娑灑洒葰傻 상〮 ˦ 些娑逤嗄𣣺沙 |- | 6:052 || ㅆ || 쌍 ˧ 槎楂苴 쌍〯 ˧˦ 槎𠞊柞 쌍〮 ˦ 乍䄍蓌 |- | 6:053 || ㆆ || ᅙᅡᆼ ˧ 阿娿痾䋪䋍鴉鴉丫啞亞 ᅙᅡᆼ〯 ˧˦ 娿婀妸❌阿猗啞瘂❌ ᅙᅡᆼ〮 ˦ 亞惡啞稏婭 |- | 6:053 || ㅎ || 항 ˧ 訶苛何呵㰤呀谺岈❌鰕 항〯 ˧˦ 㰤閜 항〮 ˦ 呵罅㙤釁❌❌嚇赫哧謑 |- | 6:054 || ㆅ || ᅘᅡᆼ ˧ 何荷河苛遐徦假蕸霞瑕鍜碬蝦鰕騢假赮 ᅘᅡᆼ〯 ˧˦ 荷何❌抲苛下夏廈 ᅘᅡᆼ〮 ˦ 賀❌荷何暇假嘉下芐夏 |- | 6:055 || ㄹ || 랑 ˧ 羅蘿籮❌鑼欏囉饠 랑〯 ˧˦ 砢❌㦬❌攞邏 |} === ㅑ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:055 || ㄱ || 걍 ˧ 迦 |- | 6:055 || ㅋ || 컁 ˧ 呿 |- | 6:055 || ㄲ || 꺙 ˧ 伽茄 |- | 6:055 || ㅈ || 쟝 ˧ 嗟罝𦋽遮庶 쟝〯 ˧˦ 姐她媎者赭堵 쟝〮 ˦ 借藉唶柘䂞蔗𤯈𤯋鷓蟅䗪炙 |- | 6:056 || ㅊ || 챵 ˧ 車 챵〯 ˧˦ 且奲撦䰩奓哆拸䞣 |- | 6:056 || ㅉ || 쨩〯 ˧˦ 抯飷 쨩〮 ˦ 藉蒩耤 |- | 6:057 || ㅅ || 샹 ˧ 些𡭟奢賖畬 샹〯 ˧˦ 寫𣞐瀉捨舍 샹〮 ˦ 卸瀉舍赦厙 |- | 6:057 || ㅆ || 썅 ˧ 邪耶斜䔑𦳃闍堵蛇虵鉈 썅〯 ˧˦ 䄬社 썅〮 ˦ 謝榭㴬射䠶麝貰 |- | 6:058 || ㅇ || 양 ˧ 邪釾鋣鎁枒椰椰耶爺斜 양〯 ˧˦ 野野也冶蠱 양〮 ˦ 夜射 |- | 6:058 || ㅿ || ᅀᅣᆼ〯 ˧˦ 惹❌若 |} === ㅘ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:058 || ㄱ || 광 ˧ 戈過撾渦❌鍋鈛❌瓜媧騧❌蝸緺 광〯 ˧˦ 果菓惈❌裹蜾裸寡❌另剮 광〮 ˦ 過裹燴 |- | 6:059 || ㅋ || 쾅 ˧ 科蝌窠薖簻誇侉胯跨姱恗夸❌荂華 쾅〯 ˧˦ 顆堁果骻胯跨㡁袴銙❌錁❌夸 쾅〮 ˦ 課堁跨❌夸胯袴❌ |- | 6:060 || ㄲ || 꽝 ˧ 瘸 |- | 6:060 || ㆁ || ᅌᅪᆼ ˧ 吪❌囮繇鈋譌訛 ᅌᅪᆼ〯 ˧˦ 瓦 ᅌᅪᆼ〮 ˦ 臥 |- | 6:060 || ㄷ || 돵 ˧ 檛薖撾 돵〯 ˧˦ 朶揣挅捶㪜崜埵𥠄鬌 |- | 6:061 || ㅌ || 퇑 ˧ 詑 퇑〯 ˧˦ 妥綏𢼻隋橢䲊墮媠 퇑〮 ˦ 唾涶佗扡拕拖大 |- | 6:061 || ㄸ || 뙁〯 ˧˦ 惰嶞墮墯隓𡐦垜𨹃䅜 뙁〮 ˦ 惰憜媠墮𢞑𢢠 |- | 6:062 || ㄴ || 놩 ˧ 捼挼 놩〮 ˦ 愞懦偄𦓏稬糯𤲬堧壖 |- | 6:062 || ㅈ || 좡 ˧ 髽 좡〮 ˦ 挫蓌夎䟶 |- | 6:062 || ㅊ || 촹〯 ˧˦ 脞 촹〮 ˦ 剉挫莝摧 |- | 6:062 || ㅉ || 쫭 ˧ 矬痤 쫭〯 ˧˦ 坐 쫭〮 ˦ 坐座 |- | 6:063 || ㅅ || 솽 ˧ 蓑莎梭𥭟唆 솽〯 ˧˦ 鎖鏁瑣璅䵀葰 |- | 6:063 || ㆆ || ᅙᅪᆼ ˧ 倭踒渦窩窊窳溛窪窐洼❌哇娃蛙䵷汙 ᅙᅪᆼ〯 ˧˦ 婐果 ᅙᅪᆼ〮 ˦ 涴汙堁 |- | 6:064 || ㅎ || 황 ˧ 鞾靴❌❌㞜花華❌ 황〯 ˧˦ 火 황〮 ˦ 貨化 |- | 6:064 || ㆅ || ᅘᅪᆼ ˧ 和龢鉌禾華崋譁嘩驊❌鋘釫鏵 ᅘᅪᆼ〯 ˧˦ 禍❌輠❌夥❌髁踝裸裸觟 ᅘᅪᆼ〮 ˦ 和華樺檴嬅摦擭獲鱯㕦 |- | 6:065 || ㄹ || 뢍 ˧ 鸁䯁騾臝蠃螺蠡蝸❌❌鏍虆蔂❌❌覼 뢍〯 ˧˦ 裸裸果臝儽蠃蓏❌蠡❌瘰攭 뢍〮 ˦ 邏摞 |} === ㅜ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:066 || ㄱ || 궁 ˧ 拘佝句❌跔駒驕痀俱㪺❌仇捄 궁〯 ˧˦ 矩榘距句拒枸蒟萬楀踽偊鄅 궁〮 ˦ 屨鞻句絇蒟瞿懼 |- | 6:067 || ㅋ || 쿵 ˧ 區驅毆敺❌歐嶇軀❌鮬摳 쿵〯 ˧˦ 齲踽 쿵〮 ˦ 驅 |- | 6:067 || ㄲ || 꿍 ˧ 劬朐❌絇句❌❌䋧軥枸鼩翑句臞癯躣躩忂灈懼戵鑺欋衢瞿鸜 꿍〯 ˧˦ 寠窶 꿍〮 ˦ 懼瞿具❌❌ |- | 6:068 || ㆁ || ᅌᅮᆼ ˧ 虞❌澞鸆麌娛禺愚隅嵎髃腢喁齵堣鍝于竽𥫡迂盂❌杅❌玗釪汙邘❌雩❌謣❌ ᅌᅮᆼ〯 ˧˦ 麌❌俁㒁羽禹䨞萭瑀偊宇㝢㡰❌雨 ᅌᅮᆼ〮 ˦ 遇寓庽禺虞芌芋𩁹吁羽雨 |- | 6:070 || ㅂ || 붕 ˧ 膚❌夫玞扶䄮鈇柎不❌枎跗趺附❌枹 붕〯 ˧˦ 甫父莆黼❌脯俌簠❌府腑俯斧鈇 붕〮 ˦ 付傅❌賦富 |- | 6:071 || ㅍ || 풍 ˧ 敷傳鋪尃溥懯孚莩罦䍖❌稃粰❌❌桴俘郛垺𡎽苻荴紨泭柎❌怤荂❌旉華麩麱䴸鄜 풍〯 ˧˦ 撫❌拊柎弣 풍〮 ˦ 赴訃仆踣掊䞳趏簠副報 |- | 6:072 || ㅃ || 뿡 ˧ 扶蚨颫夫❌芙符苻❌鳧瓿 뿡〯 ˧˦ 父㕮駁輔䩉❌鬴釜❌秿❌滏腐焤 뿡〮 ˦ 附胕柎付駙䮛鮒❌柎坿跗傅賻負負萯偩婦 |- | 6:073 || ㅁ || 뭉 ˧ 無亡蕪䍢❌璑毋巫誣 뭉〯 ˧˦ 武鵡母碔珷璑舞儛廡❌膴嫵斌憮㒇嘸甒❌❌橆蕪侮侮務䍙堥 뭉〮 ˦ 務婺瞀騖鶩霧 |- | 6:074 || ㅊ || 충 ˧ 芻䅳 |- | 6:074 || ㅉ || 쭝 ˧ 雛䅳媰 |- | 6:074 || ㅅ || 숭 ˧ 毹毺㲣𡨙氀 숭〮 ˦ 數捒 |- | 6:074 || ㆆ || ᅙᅮᆼ ˧ 紆汙迂盓陓 ᅙᅮᆼ〯 ˧˦ 傴痀❌嫗噢 ᅙᅮᆼ〮 ˦ 嫗饇歐燠噢 |- | 6:075 || ㅎ || 훙 ˧ 訏吁盱❌芋❌旴晇冔❌昫煦蓲姁喣嘔呴謳 훙〯 ˧˦ 詡栩珝訏冔❌姁昫喣煦煦膴咻 훙〮 ˦ 煦昫呴欨咻休䣱酗 |- | 6:076 || ㄹ || 룽 ˧ 慺膢褸漊鏤氀❌蔞𦭯婁 룽〯 ˧˦ 縷僂軁漊嶁㟺褸簍蔞婁 룽〮 ˦ 屢婁僂 |} === ㅠ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:077 || ㄷ || 듕 ˧ 株誅蛛跦邾禂 듕〯 ˧˦ 𪐴拄柱 듕〮 ˦ 駐軴柱住𨙦𩦛 |- | 6:077 || ㅌ || 튱 ˧ 貙 |- | 6:077 || ㄸ || 뜡 ˧ 廚躕裯幮幬趎 뜡〯 ˧˦ 柱 뜡〮 ˦ 住 |- | 6:078 || ㅈ || 즁 ˧ 諏𧩻娵朱珠侏咮祩袾 즁〯 ˧˦ 主宔砫袾麈炷枓斗 즁〮 ˦ 足注屬主註炷鉒䪒澍霔咮𠰍鑄馵 |- | 6:078 || ㅊ || 츙 ˧ 趨𨃘趣騶取樞姝 츙〯 ˧˦ 取 츙〮 ˦ 娶趣趨 |- | 6:079 || ㅉ || 쯍〯 ˧˦ 聚 쯍〮 ˦ 聚鄹𡒍㝡 |- | 6:079 || ㅅ || 슝 ˧ 須𩓣𥪙𥪥䇕𡡓鬚需繻緰㡏𦅎𪋯輸隃鄃 슝〯 ˧˦ 數籔簍藪 슝〮 ˦ 戍隃輸束 |- | 6:080 || ㅆ || 쓩 ˧ 殊銖洙㼡茱殳杸 쓩〯 ˧˦ 豎𠐊侸裋𧞫樹 쓩〮 ˦ 樹澍裋𧞫 |- | 6:080 || ㅇ || 융 ˧ 兪逾踰𧼯窬瘉瘐𨵦渝楡揄𦩞羭蝓𧍳堬瑜牏褕喩愉婾愈睮覦歈㼶臾㬰諛楰腴㥚萸 융〯 ˧˦ 庾㢏㔱楰㥚斞斔愉瘐瘉臾愈瘉癒貐窳寙 융〮 ˦ 裕䘱欲慾籲諭喩覦兪瘉 |- | 6:082 || ㅿ || ᅀᅲᆼ ˧ 儒𠍶嚅吺咮懦愞濡𣽉醹襦𧝄 ᅀᅲᆼ〯 ˧˦ 乳醹㳶 ᅀᅲᆼ〮 ˦ 孺𡦗乳 |} === ㅓ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:083 || ㄱ || 겅 ˧ 居㞐琚❌裾据椐鶋❌車 겅〯 ˧˦ 擧莒筥❌籧❌柜欅弆 겅〮 ˦ 據鐻踞倨裾鋸據居擧 |- | 6:083 || ㅋ || 컹 ˧ 墟丘嶇㠊袪祛佉胠阹呿抾魼鱋去 컹〯 ˧˦ 去麮胠 컹〮 ˦ 去㰦呿胠 |- | 6:084 || ㄲ || 껑 ˧ 渠蕖❌❌❌磲❌蘧遽籧鐻❌璩璖❌醵䣰腒 껑〯 ˧˦ 巨鉅詎渠距❌❌拒歫蚷駏❌炬❌秬岠怇苣虡簴❌❌鐻 껑〮 ˦ 遽蘧懅醵䣰勮詎巨渠 |- | 6:085 || ㆁ || ᅌᅥᆼ ˧ 魚䁩❌䐳❌漁䱷䰻齬衙 ᅌᅥᆼ〯 ˧˦ 語齬鋙❌峿敔梧衙圄圉禦御蘌❌❌❌ ᅌᅥᆼ〮 ˦ 御御禦衙圉語䱷漁 |- | 6:086 || ㆆ || ᅙᅥᆼ ˧ 於淤唹 ᅙᅥᆼ〮 ˦ 飫饇❌❌棜淤❌瘀菸 |- | 6:086 || ㅎ || 헝 ˧ 虛虗歔噓吁喣呴驉❌魖 헝〯 ˧˦ 許 헝〮 ˦ 噓 |} === ㅕ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:087 || ㄷ || 뎡 ˧ 豬猪䐗櫫瀦潴 뎡〯 ˧˦ 貯𡪄著紵䘢宁 뎡〮 ˦ 著 |- | 6:087 || ㅌ || 텽 ˧ 攄捈樗櫖摴摢㻬 텽〯 ˧˦ 楮柠褚 텽〮 ˦ 絮 |- | 6:088 || ㄸ || 뗭 ˧ 除篨滁筡著躇屠儲宁 뗭〯 ˧˦ 宁佇竚紵𦂂羜泞眝坾杼拧䇡抒苧 뗭〮 ˦ 箸櫡躇著宁除 |- | 6:088 || ㄴ || 녕 ˧ 袽帤挐拏 녕〯 ˧˦ 女 녕〮 ˦ 女 |- | 6:089 || ㅈ || 졍 ˧ 苴蒩且蛆沮諸䃴蠩 졍〯 ˧˦ 苴疽䰞𩱰𤑨煮渚陼庶 졍〮 ˦ 怚姐𢚆沮苴翥䬡庶 |- | 6:089 || ㅊ || 쳥 ˧ 疽砠❌沮趄跙狙雎苴 쳥〯 ˧˦ 且杵処處 쳥〮 ˦ 覰䁦狙蜡處 |- | 6:090 || ㅉ || 쪙〯 ˧˦ 沮咀跙 |- | 6:090 || ㅅ || 셩 ˧ 胥諝㥠胥糈稰蝑湑書𦘠舒豫𪅰紓䋡悆忬瑹荼 셩〯 ˧˦ 諝醑湑稰胥暑黍鼠癙 셩〮 ˦ 絮恕庶 |- | 6:091 || ㅆ || 쎵 ˧ 徐邪蜍蠩 쎵〯 ˧˦ 敍漵㵰序䦽㘧茅𣏗杼藇𨣦鱮魣嶼緖紓墅野杼茅𣏗桃 쎵〮 ˦ 署𥌓暏 |- | 6:092 || ㅇ || 영 ˧ 余予畬❌畭蜍餘與歟欤懙❌㦛旟璵㼂譽鸒輿❌舁伃妤茅雓 영〯 ˧˦ 與❌歟予❌ 영〮 ˦ 豫舒余與鸒譽礜藇蕷穥歟輿預忬澦❌悆 |- | 6:093 || ㄹ || 령 ˧ 臚❌盧膚驢䮫廬慮藘閭櫚 령〯 ˧˦ 呂呂侶梠膂旅❌祣❌臚穭稆儢❌ 령〮 ˦ 慮爈❌䥨鋁❌濾勴錄窶 |- | 6:094 || ㅿ || ᅀᅧᆼ ˧ 如鴐茹絮洳 ᅀᅧᆼ〯 ˧˦ 汝籹❌女茹 ᅀᅧᆼ〮 ˦ 茹洳如 |} {{단원 안내|책=동국정운 색인|상위=동국정운 색인 |이전=끝소리 ㅱ |다음=끝소리 ㆁ}} [[분류:동국정운]] a2uzj13uene7ycd6zmewdu60mv770ak 45133 45132 2024-10-31T03:02:16Z 2600:1700:C76:9010:22A7:9C6B:AF28:E81F /* ㆌ */ 45133 wikitext text/x-wiki === ㆍ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:008 || ㅈ || ᄌᆞᆼ ˧ 貲訾𤺒訿觜髭𩑽鄑咨諮資䆅姿粢秶澬姕𪗋齊𧞓齍璾齎玆茲孳滋嵫鎡仔孜耔鼒 ᄌᆞᆼ〯 ˧˦ 紫訿𤺒啙呰訾跐㧗姊秭子耔芓仔杍梓榟 ᄌᆞᆼ〮 ˦ 恣積𥡯㧘柴 |- | 5:009 || ㅊ || ᄎᆞᆼ ˧ 雌䳄郪趑次𨀥𨒮❌ ᄎᆞᆼ〯 ˧˦ 此泚佌𠈈𢇌玼皉 ᄎᆞᆼ〮 ˦ 刺𧧒庛❌𢉪次佽㐸絘䯸 |- | 5:010 || ㅉ || ᄍᆞᆼ ˧ 慈㤵磁鶿鷀鴜茲茨餈𩜴𥻓粢䭣薋𩆂䨏𩆃瓷❌薺疵玼骴髊𩨱茈𦶉訾 ᄍᆞᆼ〮 ˦ 字牸孶自漬眥眦骴髊㱴餗 |- | 5:011 || ㅅ || ᄉᆞᆼ ˧ 私思罳䰄偲愢緦颸絲司伺覗𥄶斯撕澌凘𪆁廝㒋虒傂禠㴲磃𩆵菥析徙師獅𤜳篩釃𦌿灑廝襹褷籭簛簁蓰 ᄉᆞᆼ〯 ˧˦ 徙璽死枲葸𤟧諰鰓禗躧蹝屣釃纚縰灑洒矖𥊂𩌦𩎉蓰簁籭漇史駛使 ᄉᆞᆼ〮 ˦ 賜錫瀃澌𣩠杫𣘩㮐四泗柶駟肆𩬶肄笥伺覗司僿思𩢲駛使𩌦屣灑洒曬釃䚕 |- | 5:013 || ㅆ || ᄊᆞᆼ ˧ 詞祠柌辭辤漦 ᄊᆞᆼ〯 ˧˦ 兕𧰽𠒃似仏姒㚶耜梩𦓨杞䎣枱𨐠祀𥘰𧝀汜巳士仕𣐈戺俟竢𥏳𢓪䇃𢈟𢉡逘𣀼𡱢涘騃 ᄊᆞᆼ〮 ˦ 寺嗣飤飼食飴事 |} === ㅣ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:014 || ㅋ || 킹〯 ˧˦ 企跂𠈮 킹〮 ˦ 棄企❌跂蚑 |- | 5:015 || ㄲ || 낑 ˧ 祇軝❌軙忯疧疷伎跂枝蚑𨙸岐歧祁耆鰭愭鬐 |- | 5:015 || ㄷ || 딩 ˧ 知𥎿䝷蜘𧐉鼅胝疷䟡躓𦙁 딩〯 ˧˦ 徵黹𧝉䌤緻絺希鵗 딩〮 ˦ 致致輊輖𦥎䡹質劕躓懫懥驇疐𨇈嚔❌𢷟智知置 |- | 5:016 || ㅌ || 팅 ˧ 摛攡螭彲離𡖟魑离𣉽絺瓻癡𢣕𠈴笞抬𪗪呞 팅〯 ˧˦ 褫搋拸䚦扡扯恥誀祉 팅〮 ˦ 眙 |- | 5:017 || ㄸ || 띵 ˧ 馳䮈池篪竾筂褫簃𠗺謻趨邸踟踶墀𡎰遲遟𨒈赿❌䜄坻汣𡊆沶泜蚳𧐏貾治耛菭持蛇 띵〯 ˧˦ 豸𧋈褫傂廌阤陁陊杝雉鴙鶨薙埃垁滍泜踶峙𠍰跱畤庤𤲵痔偫崻 띵〮 ˦ 地坔埊緻致穉稚䆈䄺䕌謘遲遟治値直植置褫彘 |- | 5:019 || ㄴ || 닝 ˧ 尼㞾怩跜秜旎 닝〯 ˧˦ 柅旎狔 닝〮 ˦ 膩𦡸 |- | 5:020 || ㅂ || 빙 ˧ 卑庳箄裨陴埤鞞錍椑陂波詖羆❌❌罷藣❌❌碑悲非餥裴蜚誹緋扉飛騛 빙〯 ˧˦ 俾卑髀脾鞞箄匕比妣䃾❌秕粃❌朼枇彼佊鄙啚否匪篚榧棐蜚奜蜰 빙〮 ˦ 臂嬖辟畀❌痹比庛芘賁跛詖陂披佊貱波藣祕秘泌柲鉍䪐閟毖鄪費❌粊轡䩛沸髴❌誹非芾茀 |- | 5:022 || ㅍ || 핑 ˧ 紕❌❌悂鈹鉟披㱟㨢被旇翍耚❌丕㔻伾秠怌駓䮆豾狉銔批霏䬠菲騑匪❌馡裶裴妃婓❌ 핑〯 ˧˦ 庀比庇疕仳諀吡批披㱟噽秠❌斐菲誹非悱朏 핑〮 ˦ 譬辟濞淠帔被襬費 |- | 5:024 || ㅃ || 삥 ˧ 毗紕辟𨈚❌❌阰❌魮❌鈚蚍螕琵批比膍肶㮰貔豼❌❌陴埤脾裨崥皮疲罷郫邳岯坯❌魾肥淝腓痱❌厞婓賁 삥〯 ˧˦ 婢埤䠋卑庳埤否痞岯圮䤏被骳罷陫❌ 삥〮 ˦ 鼻比紕枇𢈷庳卑避辟髲被鞁骳備俻犕糒奰贔屝厞陫❌痱腓菲蜚翡䠊剕❌費狒𥜿𥝃𦦻𤲳昲𪎰黂䆊 |- | 5:027 || ㅁ || 밍 ˧ 彌弥𢏏㜷𡝠瀰冞𥹐糜𩞁𩞇𪎭穈𪐎縻𥿫𦄐䌕靡㸏𤓒𦗕劘𪎕𠞧醾𨣿醿蘼蘪攠❌眉嵋湄𣽪𤃰攗楣郿媚麋麛黴微𧗬薇㵟溦霺 밍〯 ˧˦ 弭渳彌瀰𣴱沔敉侎𢘺芉美渼媺靡𦗕尾亹斖娓 밍〮 ˦ 寐媢媚郿篃魅靡未味 |- | 5:028 || ㅈ || 징 ˧ 支枝肢𨈪𨈙跂䧴鳷巵觶𧣨梔氏祇祗衹褆㩼多秖榰禔疧脂祗砥厎泜之芝 징〯 ˧˦ 紙帋坁汦汣❌抵觗㧗抵砥只𠮡軹枳咫𦐖❌疻旨𣅀指𢫾脂恉厎底𦒿止芷茝沚洔阯址趾畤 징〮 ˦ 寘忮伎觶𧣨觗至摯贄質鷙𪁊礩志誌識痣䏯織綕𥿮幟𢂴笫 |- | 5:031 || ㅊ || 칭 ˧ 蚩嗤𢾫媸鴟鵄眵 칭〯 ˧˦ 侈奓鉹𨪐誃哆恀䏧姼𡚼㶴袳移𧛧袲㢋㢁懘齒茝䊼 칭〮 ˦ 熾𧹹幟織旘𢂴志饎糦喜𩛉𩜮𩜂埴𡑌戠 |- | 5:032 || ㅅ || 싱 ˧ 施葹鍦鉇𥍸𥍢絁䌤𦂛䙾𧠜𧠉翅尸㕧鳲𨾋䲩屍蓍詩邿 싱〯 ˧˦ 弛𢐋㢮阤施豕矢笑屎始 싱〮 ˦ 翅𦐊翄翨𦑧施鍦釶啻翅試式弑殺煞𢎍識幟織始失 |- | 5:033 || ㅆ || 씽 ˧ 茬茌匙㮛堤提𦑡禔媞鍉時塒鰣 씽〯 ˧˦ 是諟惿媞𨸝䟗氏視市恃㤄𦧇舐𦧧咶狧 씽〮 ˦ 示謚𧨦豉嗜耆𩝙𨢍𦞯呩視侍寺䦙蒔 |- | 5:034 || ㆆ || ᅙᅵᆼ ˧ 伊洢咿吚黟黝❌ ᅙᅵᆼ〮 ˦ 縊 |- | 5:035 || ㅎ || 힝 ˧ 㕧屎䐖❌❌❌ |- | 5:035 || ㅇ || 잉 ˧ 移拖❌䔟簃❌扅❌栘鉹袲椸箷䈕㮛❌暆貤酏匜詑訑施❌❌蛇夷尼❌痍荑恞𢓡跠峓鐵侇桋䧅姨洟❌胰寅夤彛飴❌❌❌䬮怡眙詒❌貽瓵怠台頤宧圯異 잉〯 ˧˦ 以已苢苡酏祂肔胣匜迆迤施崺❌ 잉〮 ˦ 易㑥敡施袘❌緆衪貤❌移肄肆勩❌廙異異食詒貽 |- | 5:037 || ㄹ || 링 ˧ 離蘺籬杝㰚㒿䍦❌灕漓離攡摛褵縭璃䅻醨离鸝鵹❌❌❌矖纚驪孋穲䕻❌麗罹蠡盠❌劙❌棃❌棃梨犂黧❌犂❌❌犁蔾❌菞釐剺❌氂犛狸剺嫠來倈徠䅘貍狸❌猍梩❌ 링〯 ˧˦ 里理鯉❌俚悝裏李邐麗履 링〮 ˦ 詈茘離离利莅蒞位吏 |- | 5:040 || ㅿ || ᅀᅵᆼ ˧ 兒唲而胹臑腝❌❌洏聏陑栭鮞髵耏耐鴯怠❌‗ 濡 ᅀᅵᆼ〯 ˧˦ 爾爾爾邇邇邇耳耳珥駬䋙餌柅鑈檷 ᅀᅵᆼ〮 ˦ 二貳㒃樲餌㢽❌珥鉺衈咡刵毦髶聏 |} === ㆎ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:041 || ㄷ || ᄃᆡᆼ〮 ˦ 戴載襶 |- | 5:041 || ㅌ || ᄐᆡᆼ ˧ 胎孡台能駘鮐邰斄釐漦 ᄐᆡᆼ〮 ˦ 貸態詒紿怠䈚箈殆待 |- | 5:041 || ㄸ || ᄄᆡᆼ ˧ 臺𡌬薹籉儓擡鮐駘跆苔炱炲䈚箈 ᄄᆡᆼ〯 ˧˦ 待𥩳逮迨隶𨽿駘紿詒殆怠䈚箈靆 ᄄᆡᆼ〮 ˦ 代岱袋黛逮曃靆棣埭𥓏𡍖瑇❌毒玳 |- | 5:043 || ㅂ || ᄇᆡᆼ ˧ 杯盃𦈶𦈧𠤯桮 ᄇᆡᆼ〮 ˦ 背輩軰 |- | 5:043 || ㅍ || ᄑᆡᆼ ˧ 胚妚衃阫坏培醅❌ ᄑᆡᆼ〯 ˧˦ 朏 ᄑᆡᆼ〮 ˦ 配妃朏 |- | 5:043 || ㅃ || ᄈᆡᆼ ˧ 裴徘俳培陪倍阫 ᄈᆡᆼ〯 ˧˦ 琲㻗痱❌倍 ᄈᆡᆼ〮 ˦ 佩佩背偝負倍㔨北邶鄁倍焙❌孛茀勃誖悖哱怫㫲琲㻗拔萯 |- | 5:044 || ㅁ || ᄆᆡᆼ ˧ 枚玫梅楳某槑❌鋂脢脄❌膴酶䊈莓每䍙禖媒煤塺 ᄆᆡᆼ〯 ˧˦ 浼每痗脢脄 ᄆᆡᆼ〮 ˦ 妹昧㫚眛沬韎❌每痗脢脄沕琩❌冒媒 |- | 5:046 || ㅈ || ᄌᆡᆼ ˧ 哉裁栽𦳦載災灾菑甾𤆄 ᄌᆡᆼ〯 ˧˦ 宰䏁縡載 ᄌᆡᆼ〮 ˦ 再載縡 |- | 5:046 || ㅊ || ᄎᆡᆼ ˧ 猜偲 ᄎᆡᆼ〯 ˧˦ 采採寀彩棌綵䰂茝 ᄎᆡᆼ〮 ˦ 菜寀采縩 |- | 5:046 || ㅉ || ᄍᆡᆼ ˧ 裁才材財鼒纔 ᄍᆡᆼ〯 ˧˦ 在 ᄍᆡᆼ〮 ˦ 在裁栽載酨 |- | 5:047 || ㅅ || ᄉᆡᆼ ˧ 鰓䚡䰄思罳❌顋毸 ᄉᆡᆼ〮 ˦ 塞僿簺賽 |- | 5:047 || ㆆ || ᅙᆡᆼ ˧ 哀埃㶼欸唉 ᅙᆡᆼ〯 ˧˦ 欸唉款靉靄藹娭毐 ᅙᆡᆼ〮 ˦ 愛靉曖僾薆藹靄欸 |- | 5:048 || ㅎ || ᄒᆡᆼ ˧ 咍 ᄒᆡᆼ〯 ˧˦ 海❌醢❌ |- | 5:048 || ㆅ || ᅘᆡᆼ ˧ 孩㜾❌頦 ᅘᆡᆼ〯 ˧˦ 亥侅劾 ᅘᆡᆼ〮 ˦ 瀣劾 |- | 5:048 || ㄹ || ᄅᆡᆼ ˧ 來逨❌倈萊徠䅘❌騋淶崍 ᄅᆡᆼ〮 ˦ 徠倈來睞賚萊 |} === ㅢ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:049 || ㄱ || 긩 ˧ 羈䩭鞿羇踦奇倚踦奇畸觭掎剞飢饑❌肌❌姬箕❌❌箕諆❌錤棋檱踑其基朞期祺居機鐖璣禨嘰饑鞿磯譏耭蟣幾刏 긩〯 ˧˦ 己紀几機㲹麂掎踦剞❌庋攱庪㨳蟣❌穖幾 긩〮 ˦ 寄徛❌冀兾驥❌懻❌穊❌穖覬幾洎記其忌已㤅近旣漑曁 |- | 5:051 || ㅋ || 킝 ˧ 敧崎觭踦䗁徛碕榿欺倛僛❌娸❌❌❌ 킝〯 ˧˦ 綺觭起杞梩❌㞯玘芑豈䔇 킝〮 ˦ 器❌器亟氣❌乞❌ |- | 5:052 || ㄲ || 끵 ˧ 奇騎錡琦碕埼隑其萁稘期琪淇祺麒騏鶀❌旗棊碁櫀蜝綦綨❌❌𤪌璂蘄祈❌蘄肵旂頎畿圻幾❌刏❌機碕 끵〯 ˧˦ 技伎妓䗁錡跽❌ 끵〮 ˦ 芰茤騎䗁妓技❌洎垍曁❌墍蔇忌鵋誋惎㥍諅綦禨璣幾曁刏 |- | 5:054 || ㆁ || ᅌᅴᆼ ˧ 宜義儀❌檥轙議鸃䴊㕒嶬涯崖疑儗嶷觺沂凒皚溰 ᅌᅴᆼ〯 ˧˦ 蟻蟻蛾❌❌轙艤檥礒硪齮錡擬譺懝儗疑薿❌孴❌矣顗 ᅌᅴᆼ〮 ˦ 義誼❌竩議儗❌毅藙 |- | 5:055 || ㅈ || 즹 ˧ 菑載𤰭䅔𦿨淄甾緇純䊷輜椔錙鶅𨿴甾 즹〯 ˧˦ 滓胏𦞤𦙰笫緇 즹〮 ˦ 胾椔緇菑倳事輜剚𨧫 |- | 5:056 || ㅊ || 칑 ˧ 差嵯嵳䡨𨍃 칑〮 ˦ 廁厠 |- | 5:056 || ㆆ || ᅙᅴᆼ ˧ 漪猗䝝兮椅欹旖禕醫毉噫意億懿嘻譆❌衣依譩 ᅙᅴᆼ〯 ˧˦ 倚奇椅檹輢猗旖㫊䭲譩醫醷臆旖㕈庡依偯 ᅙᅴᆼ〮 ˦ 意薏倚猗輢懿饐❌饖撎衣䵝 |- | 5:058 || ㅎ || 힁 ˧ 犧曦㬢爔羲戲嚱❌❌❌❌檥巇嶬桸獻僖嬉嘻禧釐譆熹暿熺熙娭㜯稀俙晞睎豨狶欷唏鵗希 힁〯 ˧˦ 喜憘憙嬉豨唏俙霼 힁〮 ˦ 戲齂哂嚊屭豷燹咥憙喜憘嬉欷唏愾餼䊠旣熂霼墍❌❌摡❌黖 |} === ㅚ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:060 || ㄱ || 굉 ˧ 傀❌❌❌❌瑰瓌璝 굉〮 ˦ 儈會襘檜澮膾鱠獪䯤鬠❌旝鄶劊會廥憒慖幗蔮刏 |- | 5:061 || ㅋ || 쾽 ˧ 恢❌詼盔魁❌悝 쾽〮 ˦ ❌塊蕢蒯❌堁 |- | 5:061 || ㆁ || ᅌᅬᆼ ˧ 嵬峞隗阢桅磑 ᅌᅬᆼ〯 ˧˦ 隗嵬峞䫥頠 ᅌᅬᆼ〮 ˦ 外磑 |- | 5:061 || ㄷ || 됭 ˧ 鎚追❌頧搥槌磓堆❌塠㕍❌❌敦 됭〮 ˦ 祋杸對轛❌碓敦 |- | 5:062 || ㅌ || 툉 ˧ 推蓷焞 툉〯 ˧˦ 腿 툉〮 ˦ 娧脫稅駾蛻兌退𨑧𢓇 |- | 5:063 || ㄸ || 뙹 ˧ 隤頹墤穨尵僓魋 뙹〯 ˧˦ 鐓憝隊 뙹〮 ˦ 兌𩊭銳𠜑奪隊䨴𩅥𩄮薱憝譈懟憞譵鐓駾 |- | 5:063 || ㄴ || 뇡 ˧ 挼捼 뇡〯 ˧˦ 餧餒鯘鮾脮腇腇 뇡〮 ˦ 內 |- | 5:064 || ㅈ || 죙〮 ˦ 最蕞棷粹晬綷❌祽 |- | 5:064 || ㅊ || 쵱 ˧ 崔催縗衰䙑 쵱〯 ˧˦ 漼璀皠洒綷萃 쵱〮 ˦ 襊㝮竄倅萃淬焠𠗚啐啛 |- | 5:065 || ㅉ || 쬥 ˧ 摧漼凗崔㠑磪𡹐 쬥〯 ˧˦ 辠𡽕 쬥〮 ˦ 蕞 |- | 5:065 || ㅅ || 쇵 ˧ 𣯧𦸏挼䪎鞖 쇵〮 ˦ 碎粹誶 |- | 5:065 || ㆆ || ᅙᅬᆼ ˧ 隈❌椳煨䋿偎畏 ᅙᅬᆼ〯 ˧˦ 猥❌椳萎 ᅙᅬᆼ〮 ˦ 薈懀濊 |- | 5:065 || ㅎ || 횡 ˧ 灰❌䝇豗拻 횡〯 ˧˦ 賄悔 횡〮 ˦ ❌噦鐬翽誨晦悔❌靧頮 |- | 5:066 || ㆅ || ᅘᅬᆼ ˧ 回廻茴洄徊瑰槐瘣 ᅘᅬᆼ〯 ˧˦ 瘣壞廆匯滙回 ᅘᅬᆼ〮 ˦ 會禬繪䙡璯潰繪繪䜋聵瞶闠回匯䔇 |- | 5:067 || ㄹ || 룅 ˧ 雷䨓❌罍❌鑘𨯔❌礧轠瓃❌ 룅〯 ˧˦ 磊磥礌礧❌㠥礨壘櫑儡❌❌❌㒦❌蕾 룅〮 ˦ 酹類耒礧壘礌雷㵢攂 |} === ㅐ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:068 || ㄱ || 갱 ˧ 佳街皆偕湝階堦楷喈❌荄痎䕸稭祴該垓畡陔閡絯胲侅晐峐剴祴裓 갱〯 ˧˦ 解改胲❌陔閡 갱〮 ˦ 蓋蓋匃匄丐懈解繲廨解戒誡悈介界堺❌魪玠琾㠹价尬芥疥蚧❌䯰❌屆漑摡槪扢槪㧉剴 |- | 5:070 || ㅋ || 캥 ˧ 揩❌❌❌開 캥〯 ˧˦ 鍇楷愷凱豈鎧塏闓 캥〮 ˦ 磕礚愒❌渴嘅❌揩慨鎧闓欬咳愾 |- | 5:070 || ㆁ || ᅌᅢᆼ ˧ 厓崖涯漄睚倪皚❌凒敱 ᅌᅢᆼ〯 ˧˦ 騃 ᅌᅢᆼ〮 ˦ 艾❌㣻礙硋㝵儗閡 |- | 5:071 || ㄷ || 댕〮 ˦ 帶㿃蔕蹛遞 |- | 5:071 || ㅌ || 탱〮 ˦ 泰太太大忕㥭汰汏蠆𧀱蔕慸 |- | 5:071 || ㄸ || 땡〯 ˧˦ 廌豸豸 땡〮 ˦ 大軑釱汏 |- | 5:072 || ㄴ || 냉 ˧ 能 냉〯 ˧˦ 嬭㛋㚷乃迺鼐 냉〮 ˦ 柰耐𦓎能𣉘奈鼐 |- | 5:072 || ㅂ || 뱅〯 ˧˦ 擺❌捭 뱅〮 ˦ 貝狽伂沛拜扒敗 |- | 5:072 || ㅍ || 팽〮 ˦ 霈沛肺浿派湃㵒 |- | 5:073 || ㅃ || 뺑 ˧ 牌❌❌蠯螷❌排俳 뺑〯 ˧˦ 罷 뺑〮 ˦ 旆沛茷❌柭軷粺❌稗❌❌韛❌❌排糒敗唄 |- | 5:073 || ㅁ || 맹 ˧ 埋貍霾䨪 맹〯 ˧˦ 買 맹〮 ˦ 眛昧沬賣韎霾邁❌勱佅 |- | 5:074 || ㅈ || 쟁 ˧ 齋齊𩱳 쟁〯 ˧˦ 跐 쟁〮 ˦ 債責瘵祭 |- | 5:074 || ㅊ || 챙 ˧ 釵靫扠搋差䡨 챙〮 ˦ 蔡䌨縩瘥差衩𣲾 |- | 5:075 || ㅉ || 쨍 ˧ 柴祡豺犲儕 쨍〮 ˦ 眦❌疵眥砦柴 |- | 5:075 || ㅅ || 생 ˧ 簁篩 생〯 ˧˦ 灑洒躧蹝纚徙 생〮 ˦ 曬灑洒鎩𨦅殺閷煞 |- | 5:076 || ㆆ || ᅙᅢᆼ ˧ 娃哇❌挨唲 ᅙᅢᆼ〯 ˧˦ 矮㾨躷 ᅙᅢᆼ〮 ˦ 藹靄䨠馤壒❌濭隘阨搤噫❌呃嗄餲喝 |- | 5:076 || ㆅ || ᅘᅢᆼ ˧ 諧湝骸膎鞵鞋鮭 ᅘᅢᆼ〯 ˧˦ 蟹蟹獬❌鮭嶰澥解駭絯豥駴夥 ᅘᅢᆼ〮 ˦ 害妎邂解械❌薤❌瀣齘 |- | 5:077 || ㄹ || 랭〮 ˦ 賴瀨籟藾癩襰厲糲蠣賚 |} === ㅙ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:077 || ㄱ || 괭 ˧ 乖❌媧緺騧❌蝸 괭〯 ˧˦ 掛 괭〮 ˦ 卦掛挂絓罣詿怪怪壞夬澮獪㹟狤 |- | 5:078 || ㅋ || 쾡 ˧ ❌華䦱 쾡〮 ˦ 蒯❌蕢塊喟嘳㕟快噲 |- | 5:078 || ㆁ || ᅌᅫᆼ〮 ˦ 聵❌❌ |- | 5:078 || ㅊ || 쵕〮 ˦ 嘬 |- | 5:078 || ㆆ || ᅙᅫᆼ ˧ 蛙鼃洼 |- | 5:078 || ㆅ || ᅘᅫᆼ ˧ 懷櫰槐褢❌❌淮 ᅘᅫᆼ〮 ˦ 壞畵罫繣澅絓話❌躗吳 |} === ㅟ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:079 || ㄱ || 귕 ˧ 嬀潙佹龜騩歸 귕〯 ˧˦ 軌軌軌匭氿漸宄❌晷簋詭垝陒佹❌祪䤥庪庋攱傀鬼 귕〮 ˦ 媿愧❌聭騩攰庪貴瞶劌劂❌撅橛蹶❌❌鱖鱥 |- | 5:080 || ㅋ || 큉 ˧ 虧𧇾𩏣蘬巋 큉〯 ˧˦ 巋蘬 큉〮 ˦ 喟嘳㕟䙡巋缺 |- | 5:081 || ㄲ || 뀡 ˧ 逵㙺夔犪騤戣鍨頯頄馗 뀡〯 ˧˦ 跪 뀡〮 ˦ 匱鐀櫃饋歸蕢䕚簣籄㙺 |- | 5:081 || ㆁ || ᅌᅱᆼ ˧ 危峗峞帷爲韋幃褘湋違闈圍巍嵬犩❌ ᅌᅱᆼ〯 ˧˦ 頠姽峗瓦韙偉瑋煒颹暐韡❌葦 ᅌᅱᆼ〮 ˦ 僞胃❌謂渭蝟猬❌㥜媦緯圍彙❌❌魏犩❌衛熭❌❌轊䡺❌❌彘㻰 |- | 5:082 || ㄷ || 뒹〮 ˦ 轛 |- | 5:082 || ㆆ || ᅙᅱᆼ ˧ 逶倭䴧蜲痿萎委威葳蝛 ᅙᅱᆼ〯 ˧˦ 委骩磈嵬 ᅙᅱᆼ〮 ˦ 餧❌委骩尉慰蔚霨罻畏威 |- | 5:083 || ㅎ || 휭 ˧ 麾戲摩撝暉煇煒輝揮楎椲翬❌褘徽幑㫎 휭〯 ˧˦ 毁毁燬䅏❌毇❌烜䃣蟲虺蘬❌❌燬卉 휭〮 ˦ 毁諱卉 |- | 5:084 || ㅇ || 윙〯 ˧˦ 蔿蘤䦱❌❌ 윙〮 ˦ 位爲 |} === ㆌ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:084 || ㄱ || ᄀᆔᆼ ˧ 規槼摫瞡❌雉鳺 ᄀᆔᆼ〯 ˧˦ 癸湀 ᄀᆔᆼ〮 ˦ 季 |- | 5:084 || ㅋ || ᄏᆔᆼ ˧ 闚窺 ᄏᆔᆼ〯 ˧˦ 跬頃蹞窺❌頍頯 |- | 5:085 || ㄲ || ᄁᆔᆼ ˧ 葵楑鄈 ᄁᆔᆼ〯 ˧˦ 揆 ᄁᆔᆼ〮 ˦ 悸 |- | 5:085 || ㄷ || ᄃᆔᆼ ˧ 腄追䨨 |- | 5:085 || ㄸ || ᄄᆔᆼ ˧ 椎桘鎚錘槌甀魋椎鬌 ᄄᆔᆼ〮 ˦ 縋䋘槌膇㾽硾倕磓墜錘腄甀墜隊隧❌懟譵 |- | 5:086 || ㄴ || ᄂᆔᆼ〮 ˦ 諉 |- | 5:086 || ㅈ || ᄌᆔᆼ ˧ 劑檇㰎崔墔隹崒❌厜隹萑鵻騅錐 ᄌᆔᆼ〯 ˧˦ 觜❌嘴❌嶉捶棰錘箠菙 ᄌᆔᆼ〮 ˦ 醉檇雋惴諈錘 |- | 5:087 || ㅊ || ᄎᆔᆼ ˧ 吹龡炊推蓷衰 ᄎᆔᆼ〯 ˧˦ 趡揣 ᄎᆔᆼ〮 ˦ 翠臎吹䶴龡出 |- | 5:087 || ㅉ || ᄍᆔᆼ〮 ˦ 萃瘁悴顇踤 |- | 5:087 || ㅅ || ᄉᆔᆼ ˧ 綏浽荽荾䒘雖睢濉衰榱 ᄉᆔᆼ〯 ˧˦ 髓髓❌䯝䯝❌膸瀡靃❌巂嶲水準 ᄉᆔᆼ〮 ˦ 邃粹睟誶祟帥帨率 |- | 5:087 || ㅆ || ᄊᆔᆼ ˧ 隨隋遺垂陲倕❌腄誰唯譙脽 ᄊᆔᆼ〯 ˧˦ 菙❌ ᄊᆔᆼ〮 ˦ 遂燧❌鐩澻㴚襚❌璲繸❌❌隧❌隊旞❌❌彗篲穗❌穟瑞睡倕 |- | 5:089 || ㆆ || ᅙᆔᆼ〮 ˦ 恚烓 |- | 5:089 || ㅎ || ᄒᆔᆼ ˧ 墮𨼰𡐦隳隋綏觽䥴䥴睢倠 ᄒᆔᆼ〮 ˦ 睢隋綏墮挼侐 |- | 5:090 || ㅇ || ᄋᆔᆼ ˧ 惟維唯濰遺壝蠵❌❌觿 ᄋᆔᆼ〯 ˧˦ 洧鮪痏䔺❌芛芟唯踓趡壝 ᄋᆔᆼ〮 ˦ 遺❌壝瞶蜼 |- | 5:090 || ㄹ || ᄅᆔᆼ ˧ 羸纍縲累欙樏㹎❌虆藟❌蘲蔂絫 ᄅᆔᆼ〯 ˧˦ 壘藟虆蘽櫐讄❌❌❌礧❌㠥❌礨礨❌蠝❌累樏誄蜼猚 ᄅᆔᆼ〮 ˦ 類禷蘱率淚累絫 |- | 5:092 || ㅿ || ᅀᆔᆼ ˧ 甤蕤苼痿緌綏䅑䅗桵㮃 ᅀᆔᆼ〯 ˧˦ 繠橤蘂❌蕊 |} === ㅖ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:008 || ㄱ || 곙 ˧ 雞稽𮃛笄䈕枅𣓖卟乩 곙〮 ˦ 罽𦇧𣯅𦆡𣽄猘狾計繼㡭繫毄係❌髻結𨜒薊 |- | 6:008 || ㅋ || 콍 ˧ 谿溪磎嵠鸂䳶 콍〯 ˧˦ 啓棨𢧊綮启稽 콍〮 ˦ 愒憇𢠾偈❌揭䔾甈契栔鍥㓶挈 |- | 6:009 || ㄲ || 꼥〮 ˦ 偈碣嵑 |- | 6:009 || ㆁ || ᅌᅨᆼ〯 ˧˦ 掜 ᅌᅨᆼ〮 ˦ ❌甈乂刈㣻艾 |- | 6:010 || ㄷ || 뎽 ˧ 氐互低仾詆呧柢羝牴隄䧑堤鞮䩚磾 뎽〯 ˧˦ 邸䣌柢詆呧阺坻弤抵掋牴觝軝疧疷底㡳氐提堤底 뎽〮 ˦ 帝諦諟𧫚締蔕䗖柢泜氐疐嚔㗣 |- | 6:011 || ㅌ || 톙 ˧ 梯鷈鷉鵜𩀗 톙〯 ˧˦ 體軆躰涕緹紙醍 톙〮 ˦ 替暜朁涕洟鬀鬄剃殢揥楴𧝐褅裼薙雉𡲕傺袲 |- | 6:012 || ㄸ || 똉 ˧ 題提媞偍姼禔褆緹鞮睼瑅騠踶鶗嗁啼諦㖒㖷渧蹏蹄締綈䬾稊𦯔稺䄺𧀾銻𧋘鵜鴺罤䨑荑梯鮧鯷桋鴺㡗折 똉〯 ˧˦ 弟悌娣遞递𮞏 똉〮 ˦ 第弟悌娣睇眱珶遞迭递𮞏遰褅締杕軑釱踶提題鬄髢錫鶗鷤逮棣滯㿃䐭彘 |- | 6:014 || ㄴ || 녱 ˧ 泥埿臡 녱〯 ˧˦ 禰祢檷鑈鈮濔瀰薾爾泥 녱〮 ˦ 泥 |- | 6:015 || ㅂ || 볭 ˧ 篦豍❌㡙鎞❌狴❌ 볭〮 ˦ 閉箄嬖蔽草弊弊鷩鄨廢癈祓苃茀 |- | 6:015 || ㅍ || 폥 ˧ ❌批錍鈚漂 폥〮 ˦ 媲睤❌䁹辟壀埤僻濞淠潎❌肺杮 |- | 6:016 || ㅃ || 뼹 ˧ 鼙鞞𧯿椑㼰膍 뼹〯 ˧˦ 陛陛梐狴陛髀䯗䏶❌脾肶 뼹〮 ˦ 弊獘斃弊幣❌❌薜蘗❌吠犻❌茷 |- | 6:017 || ㅁ || 몡 ˧ 迷麛麑❌❌ 몡〯 ˧˦ 米眯䋛❌洣 몡〮 ˦ 袂 |- | 6:017 || ㅈ || 졩 ˧ 齎賷韲齏𦦏虀懠𢥎擠躋隮齊𨹷𢁃 졩〯 ˧˦ 濟泲㧗 졩〮 ˦ 霽擠濟𩯶祭際穄制製䱥鰶㫼晣晢淛浙折 |- | 6:018 || ㅊ || 촁 ˧ 妻萋霋凄淒悽緀 촁〯 ˧˦ 泚玼萋緀 촁〮 ˦ 切砌墄𥉻妻掣𢳅摯觢挈懘滯慸 |- | 6:018 || ㅉ || 쪵 ˧ 齊臍蠐 쪵〯 ˧˦ 薺齊癠鮆鱭 쪵〮 ˦ 嚌懠穧𨣧劑齊薺癠齌眥眦 |- | 6:019 || ㅅ || 솅 ˧ 西栖棲犀屖遟凘澌嘶撕 솅〯 ˧˦ 洗灑 솅〮 ˦ 細壻婿栖世貰勢埶殺閷 |- | 6:020 || ㅆ || 쏑 ˧ 栘 쏑〮 ˦ 誓逝遰遞筮簭噬澨遾忕 |- | 6:020 || ㆆ || ᅙᅨᆼ ˧ 鷖繄䃜瑿㙠黳 ᅙᅨᆼ〮 ˦ 瘞❌餲医瞖繄翳蘙嫕瘱曀㙪㙠殪縊 |- | 6:020 || ㅎ || 혱 ˧ 醯❌䤈橀 |- | 6:021 || ㆅ || ᅘᅨᆼ ˧ 兮❌奚㜎傒蹊❌騱貕豀謑鼷嵇 ᅘᅨᆼ〯 ˧˦ 徯諐蹊❌傒謑謑奚 ᅘᅨᆼ〮 ˦ 系繫係結禊妎盻 |- | 6:021 || ㅇ || 옝 ˧ 倪齯鯢蜺䘽輗❌棿麑猊猊霓兒 옝〮 ˦ 曳洩❌袣❌泄呭詍㖂䛖枻栧抴跇裔❌㵝㵩㳿㵩勩詣栺睨倪堄帠羿❌寱藝蓺❌槸褹袂囈 |- | 6:022 || ㄹ || 롕 ˧ 黎❌❌犁黧藜蔾瓈邌梨驪鸝蠡 롕〯 ˧˦ 禮澧醴鱧蠡䗍劙盠欐 롕〮 ˦ 麗䕻儷儷❌欐攦戾唳淚綟棙悷❌蜧茘❌蛠❌❌隷隷劙蠫❌㓯盭沴離例列栵迾裂厲厲礪❌❌糲爄勵蠣❌癘❌㾐❌ |} === ㆋ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:024 || ㄱ || ᄀᆒᆼ ˧ 圭窐❌❌❌閨袿邽蠲 ᄀᆒᆼ〮 ˦ 桂筀䳏 |- | 6:024 || ㅋ || ᄏᆒᆼ ˧ 睽聧奎暌刲㨒 |- | 6:025 || ㄷ || ᄃᆒᆼ〮 ˦ 綴餟醊腏畷錣 |- | 6:025 || ㅈ || ᄌᆒᆼ〮 ˦ 蕝蕞贅 |- | 6:025 || ㅊ || ᄎᆒᆼ〮 ˦ 脃膬毳橇竁喙 |- | 6:025 || ㅅ || ᄉᆒᆼ〮 ˦ 歲繐❌稅帨帥蛻涗裞䬽鐫說 |- | 6:025 || ㅆ || ᄊᆒᆼ〮 ˦ 篲 |- | 6:025 || ㆆ || ᅙᆒᆼ ˧ 烓❌ ᅙᆒᆼ〮 ˦ 穢薉濊獩 |- | 6:026 || ㅎ || ᄒᆒᆼ〮 ˦ 嘒嚖❌暳噦喙𣨶𤸁𠿔顪噦翽 |- | 6:026 || ㆅ || ᅘᆒᆼ ˧ 攜㩗携觿鑴蠵酅❌巂畦 ᅘᆒᆼ〮 ˦ 慧轊惠蕙譓憓 |- | 6:026 || ㅇ || ᄋᆒᆼ〮 ˦ 叡睿銳梲 |- | 6:027 || ㅿ || ᅀᆒᆼ〮 ˦ 汭內枘芮蜹 |} === ㅗ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:027 || ㄱ || 공 ˧ 孤❌觚❌柧軱呱❌箛䉉苽菰罛❌姑酤沽蛄鴣䧸辜❌❌橭❌盬夃 공〯 ˧˦ 古詁故估牯沽酤罟苦楛鼓❌瞽鼓股❌骰羖❌蠱監賈夃 공〮 ˦ 顧雇固凅錮鯝稒㧽痼故酤沽詁蠱 |- | 6:028 || ㅋ || 콩 ˧ 枯刳恗 콩〯 ˧˦ 苦䇢 콩〮 ˦ 庫袴絝胯跨❌ |- | 6:029 || ㆁ || ᅌᅩᆼ ˧ 吾浯梧鼯❌❌郚吳㻍珸鋘鋙 ᅌᅩᆼ〯 ˧˦ 五伍午迕仵旿 ᅌᅩᆼ〮 ˦ 誤悞悟晤旿晤捂梧忤牾午蘁逜寤❌遻❌迕俉愕 |- | 6:029 || ㄷ || 동 ˧ 都闍堵 동〯 ˧˦ 覩堵❌陼暏賭❌楮肚士 동〮 ˦ 妒妬奼㓃宅詫秅秺蠧螙❌斁❌睪 |- | 6:030 || ㅌ || 통 ˧ 稌 통〯 ˧˦ 土吐稌 통〮 ˦ 兎鵵菟吐 |- | 6:031 || ㄸ || 똥 ˧ 徒途墿峹𡷣荼𣘻余涂捈駼鵌鷋䅷䔑塗屠瘏菟𧈋檡兎圖鍍 똥〯 ˧˦ 杜荰土赭𢾅剫 똥〮 ˦ 度渡𣳥鍍塗 |- | 6:031 || ㄴ || 농 ˧ 奴㚢仅孥帑駑笯砮 농〯 ˧˦ 怒弩砮努 농〮 ˦ 笯怒㣽𢘂 |- | 6:032 || ㅂ || 봉 ˧ 逋餔哺誧晡 봉〯 ˧˦ 補䋠❌圃甫❌譜諩 봉〮 ˦ 布尃抪圃 |- | 6:032 || ㅍ || 퐁 ˧ 鋪痡鯆❌❌抪 퐁〯 ˧˦ 普浦誧溥 퐁〮 ˦ 怖鋪誧 |- | 6:033 || ㅃ || 뽕 ˧ 蒲莆蒱酺匍扶䒀瓿 뽕〯 ˧˦ 簿部蔀 뽕〮 ˦ 步❌荹捕哺餔䊇❌酺脯鞴 |- | 6:033 || ㅁ || 몽 ˧ 模橅❌❌謨謩嫫❌❌膜摹摸撫墓母 몽〯 ˧˦ 姥莽茻❌姆 몽〮 ˦ 暮慕募墓謨慔 |- | 6:034 || ㅈ || 종 ˧ 租蒩菹𦼬𧂚𧀽𦵔𦯓苴 종〯 ˧˦ 祖珇駔組阻岨俎柤𤕲齟詛 종〮 ˦ 作詛𥛜謯作 |- | 6:035 || ㅊ || 총 ˧ 麤麆粗觕初 총〯 ˧˦ 楚礎濋憷❌ 총〮 ˦ 措厝錯醋楚 |- | 6:035 || ㅉ || 쫑 ˧ 徂且鉏鋤助耡 쫑〯 ˧˦ 粗麆𧇿 쫑〮 ˦ 祚胙飵阼葄助耡鉏莇 |- | 6:036 || ㅅ || 송 ˧ 蘇穌㢝酥𦣑𨢭蔬疏疎𤕠梳𣐌𣓜疋綀釃斯 송〯 ˧˦ 所𢨷䝪糈 송〮 ˦ 素愫榡傃嗉訴𧪜愬㴑遡溯泝塑塐疏𥿇 |- | 6:037 || ㆆ || ᅙᅩᆼ ˧ 烏於杇圬釫❌汙汚於嗚惡 ᅙᅩᆼ〯 ˧˦ 塢䃖埡瑦鄔 ᅙᅩᆼ〮 ˦ 汙洿盓❌惡䛩䜑堊嗚 |- | 6:037 || ㅎ || 홍 ˧ 呼虖謼嘑滹❌淲❌❌泘膴幠芋 홍〯 ˧˦ 虎琥滸滹許 홍〮 ˦ 謼嘑呼戽 |- | 6:038 || ㆅ || ᅘᅩᆼ ˧ 胡❌𠴱箶❌湖瑚餬❌䭌醐❌糊粘❌❌❌䉿❌鶘乎虖壷弧狐❌瓠葫 ᅘᅩᆼ〯 ˧˦ 戶昈雇❌扈❌怙❌岵祜酤楛鄠❌下芐芦嫭嫮橭 ᅘᅩᆼ〮 ˦ 護濩頀䪝穫互枑冱瓠嫮嫭涸 |- | 6:039 || ㄹ || 롱 ˧ 盧蘆籚廬鑢爐櫨鱸壚艫轤瓐黸獹瀘矑纑顱髗❌❌玈❌❌ 롱〯 ˧˦ 魯櫓櫓㯭艣虜擄鹵滷塷瀂❌ 롱〮 ˦ 路璐露簬❌鷺❌鸕鴼潞輅賂 |} === ㅏ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:041 || ㄱ || 강 ˧ 歌謌❌戨❌牁哥柯鴚舸渮荷渮嘉佳加笳茄鴐駕珈耞跏迦痂枷葭麚豭猳家 강〯 ˧˦ 哿笴舸菏荷❌賈檟榎夏假徦嘏椵斝斝 강〮 ˦ 箇個介駕架榢枷稼嫁幏假叚下斝斝價賈 |- | 6:042 || ㅋ || 캉 ˧ 珂砢軻呿 캉〯 ˧˦ 可岢軻坷 캉〮 ˦ 軻坷髂䯊❌ |- | 6:043 || ㆁ || ᅌᅡᆼ ˧ 莪峨峨我娥俄哦睋蛾䖸鵝❌䳘鵞牙芽枒㧎衙涯厓 ᅌᅡᆼ〯 ˧˦ 我騀硪峨雅鴉庌牙疋 ᅌᅡᆼ〮 ˦ 餓訝迓御輅衙庌砑牙 |- | 6:044 || ㄷ || 당 ˧ 多多奓 당〯 ˧˦ 嚲癉哆打 당〮 ˦ 癉憚咤喥䖳㓃宅❌奼哆奓 |- | 6:044 || ㅌ || 탕 ˧ 佗他拕拖扡蛇詑訑 탕〯 ˧˦ 姹 탕〮 ˦ 詫❌咤侘差 |- | 6:045 || ㄸ || 땅 ˧ 駝駞它沱沲池陀岮陁阤跎❌佗紽酡鮀迤池鼉鱓鱣驒馱秅茶𣘻涂塗 땅〯 ˧˦ 拕拖扡佗柁舵䑨杕袉沱❌❌沲阤陀陊爹 땅〮 ˦ 馱他大 |- | 6:046 || ㄴ || 낭 ˧ 那難單儺拏挐笯 낭〯 ˧˦ 娜那袲橠難❌旎儺 낭〮 ˦ 柰那哪㖠 |- | 6:047 || ㅂ || 방 ˧ 波碆番僠嶓巴笆芭豝❌羓鈀 방〯 ˧˦ 跛❌簸播把 방〮 ˦ 播譒磻簸霸伯灞壩䃻靶弝杷欛 |- | 6:048 || ㅍ || 팡 ˧ 頗❌坡岥❌陂❌玻葩苩舥芭 팡〯 ˧˦ 頗叵 팡〮 ˦ 破帊帕袙怕❌❌ |- | 6:048 || ㅃ || 빵 ˧ 婆鄱番皤❌番杷爬把琶 빵〮 ˦ 縛杷䆉❌罷 |- | 6:049 || ㅁ || 망 ˧ 摩擵磨❌麼❌䯢魔劘攠麻菻蟆䗫蟇 망〯 ˧˦ 麼䯢馬 망〮 ˦ 磨禡貊伯榪罵䣕䣖鬕 |- | 6:049 || ㅈ || 장 ˧ 樝❌𣕈柤齟 장〯 ˧˦ 左𡯛鮓䱹𩽫苴苲 장〮 ˦ 左佐𡯛作詐醡笮苲榨𨣮𨢦溠咋 |- | 6:050 || ㅊ || 창 ˧ 蹉嵯磋瑳搓差叉杈靫扠差艖舣鎈 창〯 ˧˦ 瑳 창〮 ˦ 磋蹉差汊衩 |- | 6:051 || ㅉ || 짱 ˧ 醝鹺艖𦪸䑘㽨瘥𣩈嵳𥰭齹䣜鄼 |- | 6:051 || ㅅ || 상 ˧ 娑挱莎莏挲沙䤬𨪍桫髿𣯌犠獻戲傞𠈱沙𣲡砂紗𦀟鯊㲚髿硰 상〯 ˧˦ 娑灑洒葰傻 상〮 ˦ 些娑逤嗄𣣺沙 |- | 6:052 || ㅆ || 쌍 ˧ 槎楂苴 쌍〯 ˧˦ 槎𠞊柞 쌍〮 ˦ 乍䄍蓌 |- | 6:053 || ㆆ || ᅙᅡᆼ ˧ 阿娿痾䋪䋍鴉鴉丫啞亞 ᅙᅡᆼ〯 ˧˦ 娿婀妸❌阿猗啞瘂❌ ᅙᅡᆼ〮 ˦ 亞惡啞稏婭 |- | 6:053 || ㅎ || 항 ˧ 訶苛何呵㰤呀谺岈❌鰕 항〯 ˧˦ 㰤閜 항〮 ˦ 呵罅㙤釁❌❌嚇赫哧謑 |- | 6:054 || ㆅ || ᅘᅡᆼ ˧ 何荷河苛遐徦假蕸霞瑕鍜碬蝦鰕騢假赮 ᅘᅡᆼ〯 ˧˦ 荷何❌抲苛下夏廈 ᅘᅡᆼ〮 ˦ 賀❌荷何暇假嘉下芐夏 |- | 6:055 || ㄹ || 랑 ˧ 羅蘿籮❌鑼欏囉饠 랑〯 ˧˦ 砢❌㦬❌攞邏 |} === ㅑ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:055 || ㄱ || 걍 ˧ 迦 |- | 6:055 || ㅋ || 컁 ˧ 呿 |- | 6:055 || ㄲ || 꺙 ˧ 伽茄 |- | 6:055 || ㅈ || 쟝 ˧ 嗟罝𦋽遮庶 쟝〯 ˧˦ 姐她媎者赭堵 쟝〮 ˦ 借藉唶柘䂞蔗𤯈𤯋鷓蟅䗪炙 |- | 6:056 || ㅊ || 챵 ˧ 車 챵〯 ˧˦ 且奲撦䰩奓哆拸䞣 |- | 6:056 || ㅉ || 쨩〯 ˧˦ 抯飷 쨩〮 ˦ 藉蒩耤 |- | 6:057 || ㅅ || 샹 ˧ 些𡭟奢賖畬 샹〯 ˧˦ 寫𣞐瀉捨舍 샹〮 ˦ 卸瀉舍赦厙 |- | 6:057 || ㅆ || 썅 ˧ 邪耶斜䔑𦳃闍堵蛇虵鉈 썅〯 ˧˦ 䄬社 썅〮 ˦ 謝榭㴬射䠶麝貰 |- | 6:058 || ㅇ || 양 ˧ 邪釾鋣鎁枒椰椰耶爺斜 양〯 ˧˦ 野野也冶蠱 양〮 ˦ 夜射 |- | 6:058 || ㅿ || ᅀᅣᆼ〯 ˧˦ 惹❌若 |} === ㅘ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:058 || ㄱ || 광 ˧ 戈過撾渦❌鍋鈛❌瓜媧騧❌蝸緺 광〯 ˧˦ 果菓惈❌裹蜾裸寡❌另剮 광〮 ˦ 過裹燴 |- | 6:059 || ㅋ || 쾅 ˧ 科蝌窠薖簻誇侉胯跨姱恗夸❌荂華 쾅〯 ˧˦ 顆堁果骻胯跨㡁袴銙❌錁❌夸 쾅〮 ˦ 課堁跨❌夸胯袴❌ |- | 6:060 || ㄲ || 꽝 ˧ 瘸 |- | 6:060 || ㆁ || ᅌᅪᆼ ˧ 吪❌囮繇鈋譌訛 ᅌᅪᆼ〯 ˧˦ 瓦 ᅌᅪᆼ〮 ˦ 臥 |- | 6:060 || ㄷ || 돵 ˧ 檛薖撾 돵〯 ˧˦ 朶揣挅捶㪜崜埵𥠄鬌 |- | 6:061 || ㅌ || 퇑 ˧ 詑 퇑〯 ˧˦ 妥綏𢼻隋橢䲊墮媠 퇑〮 ˦ 唾涶佗扡拕拖大 |- | 6:061 || ㄸ || 뙁〯 ˧˦ 惰嶞墮墯隓𡐦垜𨹃䅜 뙁〮 ˦ 惰憜媠墮𢞑𢢠 |- | 6:062 || ㄴ || 놩 ˧ 捼挼 놩〮 ˦ 愞懦偄𦓏稬糯𤲬堧壖 |- | 6:062 || ㅈ || 좡 ˧ 髽 좡〮 ˦ 挫蓌夎䟶 |- | 6:062 || ㅊ || 촹〯 ˧˦ 脞 촹〮 ˦ 剉挫莝摧 |- | 6:062 || ㅉ || 쫭 ˧ 矬痤 쫭〯 ˧˦ 坐 쫭〮 ˦ 坐座 |- | 6:063 || ㅅ || 솽 ˧ 蓑莎梭𥭟唆 솽〯 ˧˦ 鎖鏁瑣璅䵀葰 |- | 6:063 || ㆆ || ᅙᅪᆼ ˧ 倭踒渦窩窊窳溛窪窐洼❌哇娃蛙䵷汙 ᅙᅪᆼ〯 ˧˦ 婐果 ᅙᅪᆼ〮 ˦ 涴汙堁 |- | 6:064 || ㅎ || 황 ˧ 鞾靴❌❌㞜花華❌ 황〯 ˧˦ 火 황〮 ˦ 貨化 |- | 6:064 || ㆅ || ᅘᅪᆼ ˧ 和龢鉌禾華崋譁嘩驊❌鋘釫鏵 ᅘᅪᆼ〯 ˧˦ 禍❌輠❌夥❌髁踝裸裸觟 ᅘᅪᆼ〮 ˦ 和華樺檴嬅摦擭獲鱯㕦 |- | 6:065 || ㄹ || 뢍 ˧ 鸁䯁騾臝蠃螺蠡蝸❌❌鏍虆蔂❌❌覼 뢍〯 ˧˦ 裸裸果臝儽蠃蓏❌蠡❌瘰攭 뢍〮 ˦ 邏摞 |} === ㅜ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:066 || ㄱ || 궁 ˧ 拘佝句❌跔駒驕痀俱㪺❌仇捄 궁〯 ˧˦ 矩榘距句拒枸蒟萬楀踽偊鄅 궁〮 ˦ 屨鞻句絇蒟瞿懼 |- | 6:067 || ㅋ || 쿵 ˧ 區驅毆敺❌歐嶇軀❌鮬摳 쿵〯 ˧˦ 齲踽 쿵〮 ˦ 驅 |- | 6:067 || ㄲ || 꿍 ˧ 劬朐❌絇句❌❌䋧軥枸鼩翑句臞癯躣躩忂灈懼戵鑺欋衢瞿鸜 꿍〯 ˧˦ 寠窶 꿍〮 ˦ 懼瞿具❌❌ |- | 6:068 || ㆁ || ᅌᅮᆼ ˧ 虞❌澞鸆麌娛禺愚隅嵎髃腢喁齵堣鍝于竽𥫡迂盂❌杅❌玗釪汙邘❌雩❌謣❌ ᅌᅮᆼ〯 ˧˦ 麌❌俁㒁羽禹䨞萭瑀偊宇㝢㡰❌雨 ᅌᅮᆼ〮 ˦ 遇寓庽禺虞芌芋𩁹吁羽雨 |- | 6:070 || ㅂ || 붕 ˧ 膚❌夫玞扶䄮鈇柎不❌枎跗趺附❌枹 붕〯 ˧˦ 甫父莆黼❌脯俌簠❌府腑俯斧鈇 붕〮 ˦ 付傅❌賦富 |- | 6:071 || ㅍ || 풍 ˧ 敷傳鋪尃溥懯孚莩罦䍖❌稃粰❌❌桴俘郛垺𡎽苻荴紨泭柎❌怤荂❌旉華麩麱䴸鄜 풍〯 ˧˦ 撫❌拊柎弣 풍〮 ˦ 赴訃仆踣掊䞳趏簠副報 |- | 6:072 || ㅃ || 뿡 ˧ 扶蚨颫夫❌芙符苻❌鳧瓿 뿡〯 ˧˦ 父㕮駁輔䩉❌鬴釜❌秿❌滏腐焤 뿡〮 ˦ 附胕柎付駙䮛鮒❌柎坿跗傅賻負負萯偩婦 |- | 6:073 || ㅁ || 뭉 ˧ 無亡蕪䍢❌璑毋巫誣 뭉〯 ˧˦ 武鵡母碔珷璑舞儛廡❌膴嫵斌憮㒇嘸甒❌❌橆蕪侮侮務䍙堥 뭉〮 ˦ 務婺瞀騖鶩霧 |- | 6:074 || ㅊ || 충 ˧ 芻䅳 |- | 6:074 || ㅉ || 쭝 ˧ 雛䅳媰 |- | 6:074 || ㅅ || 숭 ˧ 毹毺㲣𡨙氀 숭〮 ˦ 數捒 |- | 6:074 || ㆆ || ᅙᅮᆼ ˧ 紆汙迂盓陓 ᅙᅮᆼ〯 ˧˦ 傴痀❌嫗噢 ᅙᅮᆼ〮 ˦ 嫗饇歐燠噢 |- | 6:075 || ㅎ || 훙 ˧ 訏吁盱❌芋❌旴晇冔❌昫煦蓲姁喣嘔呴謳 훙〯 ˧˦ 詡栩珝訏冔❌姁昫喣煦煦膴咻 훙〮 ˦ 煦昫呴欨咻休䣱酗 |- | 6:076 || ㄹ || 룽 ˧ 慺膢褸漊鏤氀❌蔞𦭯婁 룽〯 ˧˦ 縷僂軁漊嶁㟺褸簍蔞婁 룽〮 ˦ 屢婁僂 |} === ㅠ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:077 || ㄷ || 듕 ˧ 株誅蛛跦邾禂 듕〯 ˧˦ 𪐴拄柱 듕〮 ˦ 駐軴柱住𨙦𩦛 |- | 6:077 || ㅌ || 튱 ˧ 貙 |- | 6:077 || ㄸ || 뜡 ˧ 廚躕裯幮幬趎 뜡〯 ˧˦ 柱 뜡〮 ˦ 住 |- | 6:078 || ㅈ || 즁 ˧ 諏𧩻娵朱珠侏咮祩袾 즁〯 ˧˦ 主宔砫袾麈炷枓斗 즁〮 ˦ 足注屬主註炷鉒䪒澍霔咮𠰍鑄馵 |- | 6:078 || ㅊ || 츙 ˧ 趨𨃘趣騶取樞姝 츙〯 ˧˦ 取 츙〮 ˦ 娶趣趨 |- | 6:079 || ㅉ || 쯍〯 ˧˦ 聚 쯍〮 ˦ 聚鄹𡒍㝡 |- | 6:079 || ㅅ || 슝 ˧ 須𩓣𥪙𥪥䇕𡡓鬚需繻緰㡏𦅎𪋯輸隃鄃 슝〯 ˧˦ 數籔簍藪 슝〮 ˦ 戍隃輸束 |- | 6:080 || ㅆ || 쓩 ˧ 殊銖洙㼡茱殳杸 쓩〯 ˧˦ 豎𠐊侸裋𧞫樹 쓩〮 ˦ 樹澍裋𧞫 |- | 6:080 || ㅇ || 융 ˧ 兪逾踰𧼯窬瘉瘐𨵦渝楡揄𦩞羭蝓𧍳堬瑜牏褕喩愉婾愈睮覦歈㼶臾㬰諛楰腴㥚萸 융〯 ˧˦ 庾㢏㔱楰㥚斞斔愉瘐瘉臾愈瘉癒貐窳寙 융〮 ˦ 裕䘱欲慾籲諭喩覦兪瘉 |- | 6:082 || ㅿ || ᅀᅲᆼ ˧ 儒𠍶嚅吺咮懦愞濡𣽉醹襦𧝄 ᅀᅲᆼ〯 ˧˦ 乳醹㳶 ᅀᅲᆼ〮 ˦ 孺𡦗乳 |} === ㅓ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:083 || ㄱ || 겅 ˧ 居㞐琚❌裾据椐鶋❌車 겅〯 ˧˦ 擧莒筥❌籧❌柜欅弆 겅〮 ˦ 據鐻踞倨裾鋸據居擧 |- | 6:083 || ㅋ || 컹 ˧ 墟丘嶇㠊袪祛佉胠阹呿抾魼鱋去 컹〯 ˧˦ 去麮胠 컹〮 ˦ 去㰦呿胠 |- | 6:084 || ㄲ || 껑 ˧ 渠蕖❌❌❌磲❌蘧遽籧鐻❌璩璖❌醵䣰腒 껑〯 ˧˦ 巨鉅詎渠距❌❌拒歫蚷駏❌炬❌秬岠怇苣虡簴❌❌鐻 껑〮 ˦ 遽蘧懅醵䣰勮詎巨渠 |- | 6:085 || ㆁ || ᅌᅥᆼ ˧ 魚䁩❌䐳❌漁䱷䰻齬衙 ᅌᅥᆼ〯 ˧˦ 語齬鋙❌峿敔梧衙圄圉禦御蘌❌❌❌ ᅌᅥᆼ〮 ˦ 御御禦衙圉語䱷漁 |- | 6:086 || ㆆ || ᅙᅥᆼ ˧ 於淤唹 ᅙᅥᆼ〮 ˦ 飫饇❌❌棜淤❌瘀菸 |- | 6:086 || ㅎ || 헝 ˧ 虛虗歔噓吁喣呴驉❌魖 헝〯 ˧˦ 許 헝〮 ˦ 噓 |} === ㅕ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:087 || ㄷ || 뎡 ˧ 豬猪䐗櫫瀦潴 뎡〯 ˧˦ 貯𡪄著紵䘢宁 뎡〮 ˦ 著 |- | 6:087 || ㅌ || 텽 ˧ 攄捈樗櫖摴摢㻬 텽〯 ˧˦ 楮柠褚 텽〮 ˦ 絮 |- | 6:088 || ㄸ || 뗭 ˧ 除篨滁筡著躇屠儲宁 뗭〯 ˧˦ 宁佇竚紵𦂂羜泞眝坾杼拧䇡抒苧 뗭〮 ˦ 箸櫡躇著宁除 |- | 6:088 || ㄴ || 녕 ˧ 袽帤挐拏 녕〯 ˧˦ 女 녕〮 ˦ 女 |- | 6:089 || ㅈ || 졍 ˧ 苴蒩且蛆沮諸䃴蠩 졍〯 ˧˦ 苴疽䰞𩱰𤑨煮渚陼庶 졍〮 ˦ 怚姐𢚆沮苴翥䬡庶 |- | 6:089 || ㅊ || 쳥 ˧ 疽砠❌沮趄跙狙雎苴 쳥〯 ˧˦ 且杵処處 쳥〮 ˦ 覰䁦狙蜡處 |- | 6:090 || ㅉ || 쪙〯 ˧˦ 沮咀跙 |- | 6:090 || ㅅ || 셩 ˧ 胥諝㥠胥糈稰蝑湑書𦘠舒豫𪅰紓䋡悆忬瑹荼 셩〯 ˧˦ 諝醑湑稰胥暑黍鼠癙 셩〮 ˦ 絮恕庶 |- | 6:091 || ㅆ || 쎵 ˧ 徐邪蜍蠩 쎵〯 ˧˦ 敍漵㵰序䦽㘧茅𣏗杼藇𨣦鱮魣嶼緖紓墅野杼茅𣏗桃 쎵〮 ˦ 署𥌓暏 |- | 6:092 || ㅇ || 영 ˧ 余予畬❌畭蜍餘與歟欤懙❌㦛旟璵㼂譽鸒輿❌舁伃妤茅雓 영〯 ˧˦ 與❌歟予❌ 영〮 ˦ 豫舒余與鸒譽礜藇蕷穥歟輿預忬澦❌悆 |- | 6:093 || ㄹ || 령 ˧ 臚❌盧膚驢䮫廬慮藘閭櫚 령〯 ˧˦ 呂呂侶梠膂旅❌祣❌臚穭稆儢❌ 령〮 ˦ 慮爈❌䥨鋁❌濾勴錄窶 |- | 6:094 || ㅿ || ᅀᅧᆼ ˧ 如鴐茹絮洳 ᅀᅧᆼ〯 ˧˦ 汝籹❌女茹 ᅀᅧᆼ〮 ˦ 茹洳如 |} {{단원 안내|책=동국정운 색인|상위=동국정운 색인 |이전=끝소리 ㅱ |다음=끝소리 ㆁ}} [[분류:동국정운]] 9n2ot146rzj9w2b5ayecd05jcus6l18 45134 45133 2024-10-31T03:03:25Z 2600:1700:C76:9010:22A7:9C6B:AF28:E81F /* ㆋ */ 45134 wikitext text/x-wiki === ㆍ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:008 || ㅈ || ᄌᆞᆼ ˧ 貲訾𤺒訿觜髭𩑽鄑咨諮資䆅姿粢秶澬姕𪗋齊𧞓齍璾齎玆茲孳滋嵫鎡仔孜耔鼒 ᄌᆞᆼ〯 ˧˦ 紫訿𤺒啙呰訾跐㧗姊秭子耔芓仔杍梓榟 ᄌᆞᆼ〮 ˦ 恣積𥡯㧘柴 |- | 5:009 || ㅊ || ᄎᆞᆼ ˧ 雌䳄郪趑次𨀥𨒮❌ ᄎᆞᆼ〯 ˧˦ 此泚佌𠈈𢇌玼皉 ᄎᆞᆼ〮 ˦ 刺𧧒庛❌𢉪次佽㐸絘䯸 |- | 5:010 || ㅉ || ᄍᆞᆼ ˧ 慈㤵磁鶿鷀鴜茲茨餈𩜴𥻓粢䭣薋𩆂䨏𩆃瓷❌薺疵玼骴髊𩨱茈𦶉訾 ᄍᆞᆼ〮 ˦ 字牸孶自漬眥眦骴髊㱴餗 |- | 5:011 || ㅅ || ᄉᆞᆼ ˧ 私思罳䰄偲愢緦颸絲司伺覗𥄶斯撕澌凘𪆁廝㒋虒傂禠㴲磃𩆵菥析徙師獅𤜳篩釃𦌿灑廝襹褷籭簛簁蓰 ᄉᆞᆼ〯 ˧˦ 徙璽死枲葸𤟧諰鰓禗躧蹝屣釃纚縰灑洒矖𥊂𩌦𩎉蓰簁籭漇史駛使 ᄉᆞᆼ〮 ˦ 賜錫瀃澌𣩠杫𣘩㮐四泗柶駟肆𩬶肄笥伺覗司僿思𩢲駛使𩌦屣灑洒曬釃䚕 |- | 5:013 || ㅆ || ᄊᆞᆼ ˧ 詞祠柌辭辤漦 ᄊᆞᆼ〯 ˧˦ 兕𧰽𠒃似仏姒㚶耜梩𦓨杞䎣枱𨐠祀𥘰𧝀汜巳士仕𣐈戺俟竢𥏳𢓪䇃𢈟𢉡逘𣀼𡱢涘騃 ᄊᆞᆼ〮 ˦ 寺嗣飤飼食飴事 |} === ㅣ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:014 || ㅋ || 킹〯 ˧˦ 企跂𠈮 킹〮 ˦ 棄企❌跂蚑 |- | 5:015 || ㄲ || 낑 ˧ 祇軝❌軙忯疧疷伎跂枝蚑𨙸岐歧祁耆鰭愭鬐 |- | 5:015 || ㄷ || 딩 ˧ 知𥎿䝷蜘𧐉鼅胝疷䟡躓𦙁 딩〯 ˧˦ 徵黹𧝉䌤緻絺希鵗 딩〮 ˦ 致致輊輖𦥎䡹質劕躓懫懥驇疐𨇈嚔❌𢷟智知置 |- | 5:016 || ㅌ || 팅 ˧ 摛攡螭彲離𡖟魑离𣉽絺瓻癡𢣕𠈴笞抬𪗪呞 팅〯 ˧˦ 褫搋拸䚦扡扯恥誀祉 팅〮 ˦ 眙 |- | 5:017 || ㄸ || 띵 ˧ 馳䮈池篪竾筂褫簃𠗺謻趨邸踟踶墀𡎰遲遟𨒈赿❌䜄坻汣𡊆沶泜蚳𧐏貾治耛菭持蛇 띵〯 ˧˦ 豸𧋈褫傂廌阤陁陊杝雉鴙鶨薙埃垁滍泜踶峙𠍰跱畤庤𤲵痔偫崻 띵〮 ˦ 地坔埊緻致穉稚䆈䄺䕌謘遲遟治値直植置褫彘 |- | 5:019 || ㄴ || 닝 ˧ 尼㞾怩跜秜旎 닝〯 ˧˦ 柅旎狔 닝〮 ˦ 膩𦡸 |- | 5:020 || ㅂ || 빙 ˧ 卑庳箄裨陴埤鞞錍椑陂波詖羆❌❌罷藣❌❌碑悲非餥裴蜚誹緋扉飛騛 빙〯 ˧˦ 俾卑髀脾鞞箄匕比妣䃾❌秕粃❌朼枇彼佊鄙啚否匪篚榧棐蜚奜蜰 빙〮 ˦ 臂嬖辟畀❌痹比庛芘賁跛詖陂披佊貱波藣祕秘泌柲鉍䪐閟毖鄪費❌粊轡䩛沸髴❌誹非芾茀 |- | 5:022 || ㅍ || 핑 ˧ 紕❌❌悂鈹鉟披㱟㨢被旇翍耚❌丕㔻伾秠怌駓䮆豾狉銔批霏䬠菲騑匪❌馡裶裴妃婓❌ 핑〯 ˧˦ 庀比庇疕仳諀吡批披㱟噽秠❌斐菲誹非悱朏 핑〮 ˦ 譬辟濞淠帔被襬費 |- | 5:024 || ㅃ || 삥 ˧ 毗紕辟𨈚❌❌阰❌魮❌鈚蚍螕琵批比膍肶㮰貔豼❌❌陴埤脾裨崥皮疲罷郫邳岯坯❌魾肥淝腓痱❌厞婓賁 삥〯 ˧˦ 婢埤䠋卑庳埤否痞岯圮䤏被骳罷陫❌ 삥〮 ˦ 鼻比紕枇𢈷庳卑避辟髲被鞁骳備俻犕糒奰贔屝厞陫❌痱腓菲蜚翡䠊剕❌費狒𥜿𥝃𦦻𤲳昲𪎰黂䆊 |- | 5:027 || ㅁ || 밍 ˧ 彌弥𢏏㜷𡝠瀰冞𥹐糜𩞁𩞇𪎭穈𪐎縻𥿫𦄐䌕靡㸏𤓒𦗕劘𪎕𠞧醾𨣿醿蘼蘪攠❌眉嵋湄𣽪𤃰攗楣郿媚麋麛黴微𧗬薇㵟溦霺 밍〯 ˧˦ 弭渳彌瀰𣴱沔敉侎𢘺芉美渼媺靡𦗕尾亹斖娓 밍〮 ˦ 寐媢媚郿篃魅靡未味 |- | 5:028 || ㅈ || 징 ˧ 支枝肢𨈪𨈙跂䧴鳷巵觶𧣨梔氏祇祗衹褆㩼多秖榰禔疧脂祗砥厎泜之芝 징〯 ˧˦ 紙帋坁汦汣❌抵觗㧗抵砥只𠮡軹枳咫𦐖❌疻旨𣅀指𢫾脂恉厎底𦒿止芷茝沚洔阯址趾畤 징〮 ˦ 寘忮伎觶𧣨觗至摯贄質鷙𪁊礩志誌識痣䏯織綕𥿮幟𢂴笫 |- | 5:031 || ㅊ || 칭 ˧ 蚩嗤𢾫媸鴟鵄眵 칭〯 ˧˦ 侈奓鉹𨪐誃哆恀䏧姼𡚼㶴袳移𧛧袲㢋㢁懘齒茝䊼 칭〮 ˦ 熾𧹹幟織旘𢂴志饎糦喜𩛉𩜮𩜂埴𡑌戠 |- | 5:032 || ㅅ || 싱 ˧ 施葹鍦鉇𥍸𥍢絁䌤𦂛䙾𧠜𧠉翅尸㕧鳲𨾋䲩屍蓍詩邿 싱〯 ˧˦ 弛𢐋㢮阤施豕矢笑屎始 싱〮 ˦ 翅𦐊翄翨𦑧施鍦釶啻翅試式弑殺煞𢎍識幟織始失 |- | 5:033 || ㅆ || 씽 ˧ 茬茌匙㮛堤提𦑡禔媞鍉時塒鰣 씽〯 ˧˦ 是諟惿媞𨸝䟗氏視市恃㤄𦧇舐𦧧咶狧 씽〮 ˦ 示謚𧨦豉嗜耆𩝙𨢍𦞯呩視侍寺䦙蒔 |- | 5:034 || ㆆ || ᅙᅵᆼ ˧ 伊洢咿吚黟黝❌ ᅙᅵᆼ〮 ˦ 縊 |- | 5:035 || ㅎ || 힝 ˧ 㕧屎䐖❌❌❌ |- | 5:035 || ㅇ || 잉 ˧ 移拖❌䔟簃❌扅❌栘鉹袲椸箷䈕㮛❌暆貤酏匜詑訑施❌❌蛇夷尼❌痍荑恞𢓡跠峓鐵侇桋䧅姨洟❌胰寅夤彛飴❌❌❌䬮怡眙詒❌貽瓵怠台頤宧圯異 잉〯 ˧˦ 以已苢苡酏祂肔胣匜迆迤施崺❌ 잉〮 ˦ 易㑥敡施袘❌緆衪貤❌移肄肆勩❌廙異異食詒貽 |- | 5:037 || ㄹ || 링 ˧ 離蘺籬杝㰚㒿䍦❌灕漓離攡摛褵縭璃䅻醨离鸝鵹❌❌❌矖纚驪孋穲䕻❌麗罹蠡盠❌劙❌棃❌棃梨犂黧❌犂❌❌犁蔾❌菞釐剺❌氂犛狸剺嫠來倈徠䅘貍狸❌猍梩❌ 링〯 ˧˦ 里理鯉❌俚悝裏李邐麗履 링〮 ˦ 詈茘離离利莅蒞位吏 |- | 5:040 || ㅿ || ᅀᅵᆼ ˧ 兒唲而胹臑腝❌❌洏聏陑栭鮞髵耏耐鴯怠❌‗ 濡 ᅀᅵᆼ〯 ˧˦ 爾爾爾邇邇邇耳耳珥駬䋙餌柅鑈檷 ᅀᅵᆼ〮 ˦ 二貳㒃樲餌㢽❌珥鉺衈咡刵毦髶聏 |} === ㆎ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:041 || ㄷ || ᄃᆡᆼ〮 ˦ 戴載襶 |- | 5:041 || ㅌ || ᄐᆡᆼ ˧ 胎孡台能駘鮐邰斄釐漦 ᄐᆡᆼ〮 ˦ 貸態詒紿怠䈚箈殆待 |- | 5:041 || ㄸ || ᄄᆡᆼ ˧ 臺𡌬薹籉儓擡鮐駘跆苔炱炲䈚箈 ᄄᆡᆼ〯 ˧˦ 待𥩳逮迨隶𨽿駘紿詒殆怠䈚箈靆 ᄄᆡᆼ〮 ˦ 代岱袋黛逮曃靆棣埭𥓏𡍖瑇❌毒玳 |- | 5:043 || ㅂ || ᄇᆡᆼ ˧ 杯盃𦈶𦈧𠤯桮 ᄇᆡᆼ〮 ˦ 背輩軰 |- | 5:043 || ㅍ || ᄑᆡᆼ ˧ 胚妚衃阫坏培醅❌ ᄑᆡᆼ〯 ˧˦ 朏 ᄑᆡᆼ〮 ˦ 配妃朏 |- | 5:043 || ㅃ || ᄈᆡᆼ ˧ 裴徘俳培陪倍阫 ᄈᆡᆼ〯 ˧˦ 琲㻗痱❌倍 ᄈᆡᆼ〮 ˦ 佩佩背偝負倍㔨北邶鄁倍焙❌孛茀勃誖悖哱怫㫲琲㻗拔萯 |- | 5:044 || ㅁ || ᄆᆡᆼ ˧ 枚玫梅楳某槑❌鋂脢脄❌膴酶䊈莓每䍙禖媒煤塺 ᄆᆡᆼ〯 ˧˦ 浼每痗脢脄 ᄆᆡᆼ〮 ˦ 妹昧㫚眛沬韎❌每痗脢脄沕琩❌冒媒 |- | 5:046 || ㅈ || ᄌᆡᆼ ˧ 哉裁栽𦳦載災灾菑甾𤆄 ᄌᆡᆼ〯 ˧˦ 宰䏁縡載 ᄌᆡᆼ〮 ˦ 再載縡 |- | 5:046 || ㅊ || ᄎᆡᆼ ˧ 猜偲 ᄎᆡᆼ〯 ˧˦ 采採寀彩棌綵䰂茝 ᄎᆡᆼ〮 ˦ 菜寀采縩 |- | 5:046 || ㅉ || ᄍᆡᆼ ˧ 裁才材財鼒纔 ᄍᆡᆼ〯 ˧˦ 在 ᄍᆡᆼ〮 ˦ 在裁栽載酨 |- | 5:047 || ㅅ || ᄉᆡᆼ ˧ 鰓䚡䰄思罳❌顋毸 ᄉᆡᆼ〮 ˦ 塞僿簺賽 |- | 5:047 || ㆆ || ᅙᆡᆼ ˧ 哀埃㶼欸唉 ᅙᆡᆼ〯 ˧˦ 欸唉款靉靄藹娭毐 ᅙᆡᆼ〮 ˦ 愛靉曖僾薆藹靄欸 |- | 5:048 || ㅎ || ᄒᆡᆼ ˧ 咍 ᄒᆡᆼ〯 ˧˦ 海❌醢❌ |- | 5:048 || ㆅ || ᅘᆡᆼ ˧ 孩㜾❌頦 ᅘᆡᆼ〯 ˧˦ 亥侅劾 ᅘᆡᆼ〮 ˦ 瀣劾 |- | 5:048 || ㄹ || ᄅᆡᆼ ˧ 來逨❌倈萊徠䅘❌騋淶崍 ᄅᆡᆼ〮 ˦ 徠倈來睞賚萊 |} === ㅢ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:049 || ㄱ || 긩 ˧ 羈䩭鞿羇踦奇倚踦奇畸觭掎剞飢饑❌肌❌姬箕❌❌箕諆❌錤棋檱踑其基朞期祺居機鐖璣禨嘰饑鞿磯譏耭蟣幾刏 긩〯 ˧˦ 己紀几機㲹麂掎踦剞❌庋攱庪㨳蟣❌穖幾 긩〮 ˦ 寄徛❌冀兾驥❌懻❌穊❌穖覬幾洎記其忌已㤅近旣漑曁 |- | 5:051 || ㅋ || 킝 ˧ 敧崎觭踦䗁徛碕榿欺倛僛❌娸❌❌❌ 킝〯 ˧˦ 綺觭起杞梩❌㞯玘芑豈䔇 킝〮 ˦ 器❌器亟氣❌乞❌ |- | 5:052 || ㄲ || 끵 ˧ 奇騎錡琦碕埼隑其萁稘期琪淇祺麒騏鶀❌旗棊碁櫀蜝綦綨❌❌𤪌璂蘄祈❌蘄肵旂頎畿圻幾❌刏❌機碕 끵〯 ˧˦ 技伎妓䗁錡跽❌ 끵〮 ˦ 芰茤騎䗁妓技❌洎垍曁❌墍蔇忌鵋誋惎㥍諅綦禨璣幾曁刏 |- | 5:054 || ㆁ || ᅌᅴᆼ ˧ 宜義儀❌檥轙議鸃䴊㕒嶬涯崖疑儗嶷觺沂凒皚溰 ᅌᅴᆼ〯 ˧˦ 蟻蟻蛾❌❌轙艤檥礒硪齮錡擬譺懝儗疑薿❌孴❌矣顗 ᅌᅴᆼ〮 ˦ 義誼❌竩議儗❌毅藙 |- | 5:055 || ㅈ || 즹 ˧ 菑載𤰭䅔𦿨淄甾緇純䊷輜椔錙鶅𨿴甾 즹〯 ˧˦ 滓胏𦞤𦙰笫緇 즹〮 ˦ 胾椔緇菑倳事輜剚𨧫 |- | 5:056 || ㅊ || 칑 ˧ 差嵯嵳䡨𨍃 칑〮 ˦ 廁厠 |- | 5:056 || ㆆ || ᅙᅴᆼ ˧ 漪猗䝝兮椅欹旖禕醫毉噫意億懿嘻譆❌衣依譩 ᅙᅴᆼ〯 ˧˦ 倚奇椅檹輢猗旖㫊䭲譩醫醷臆旖㕈庡依偯 ᅙᅴᆼ〮 ˦ 意薏倚猗輢懿饐❌饖撎衣䵝 |- | 5:058 || ㅎ || 힁 ˧ 犧曦㬢爔羲戲嚱❌❌❌❌檥巇嶬桸獻僖嬉嘻禧釐譆熹暿熺熙娭㜯稀俙晞睎豨狶欷唏鵗希 힁〯 ˧˦ 喜憘憙嬉豨唏俙霼 힁〮 ˦ 戲齂哂嚊屭豷燹咥憙喜憘嬉欷唏愾餼䊠旣熂霼墍❌❌摡❌黖 |} === ㅚ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:060 || ㄱ || 굉 ˧ 傀❌❌❌❌瑰瓌璝 굉〮 ˦ 儈會襘檜澮膾鱠獪䯤鬠❌旝鄶劊會廥憒慖幗蔮刏 |- | 5:061 || ㅋ || 쾽 ˧ 恢❌詼盔魁❌悝 쾽〮 ˦ ❌塊蕢蒯❌堁 |- | 5:061 || ㆁ || ᅌᅬᆼ ˧ 嵬峞隗阢桅磑 ᅌᅬᆼ〯 ˧˦ 隗嵬峞䫥頠 ᅌᅬᆼ〮 ˦ 外磑 |- | 5:061 || ㄷ || 됭 ˧ 鎚追❌頧搥槌磓堆❌塠㕍❌❌敦 됭〮 ˦ 祋杸對轛❌碓敦 |- | 5:062 || ㅌ || 툉 ˧ 推蓷焞 툉〯 ˧˦ 腿 툉〮 ˦ 娧脫稅駾蛻兌退𨑧𢓇 |- | 5:063 || ㄸ || 뙹 ˧ 隤頹墤穨尵僓魋 뙹〯 ˧˦ 鐓憝隊 뙹〮 ˦ 兌𩊭銳𠜑奪隊䨴𩅥𩄮薱憝譈懟憞譵鐓駾 |- | 5:063 || ㄴ || 뇡 ˧ 挼捼 뇡〯 ˧˦ 餧餒鯘鮾脮腇腇 뇡〮 ˦ 內 |- | 5:064 || ㅈ || 죙〮 ˦ 最蕞棷粹晬綷❌祽 |- | 5:064 || ㅊ || 쵱 ˧ 崔催縗衰䙑 쵱〯 ˧˦ 漼璀皠洒綷萃 쵱〮 ˦ 襊㝮竄倅萃淬焠𠗚啐啛 |- | 5:065 || ㅉ || 쬥 ˧ 摧漼凗崔㠑磪𡹐 쬥〯 ˧˦ 辠𡽕 쬥〮 ˦ 蕞 |- | 5:065 || ㅅ || 쇵 ˧ 𣯧𦸏挼䪎鞖 쇵〮 ˦ 碎粹誶 |- | 5:065 || ㆆ || ᅙᅬᆼ ˧ 隈❌椳煨䋿偎畏 ᅙᅬᆼ〯 ˧˦ 猥❌椳萎 ᅙᅬᆼ〮 ˦ 薈懀濊 |- | 5:065 || ㅎ || 횡 ˧ 灰❌䝇豗拻 횡〯 ˧˦ 賄悔 횡〮 ˦ ❌噦鐬翽誨晦悔❌靧頮 |- | 5:066 || ㆅ || ᅘᅬᆼ ˧ 回廻茴洄徊瑰槐瘣 ᅘᅬᆼ〯 ˧˦ 瘣壞廆匯滙回 ᅘᅬᆼ〮 ˦ 會禬繪䙡璯潰繪繪䜋聵瞶闠回匯䔇 |- | 5:067 || ㄹ || 룅 ˧ 雷䨓❌罍❌鑘𨯔❌礧轠瓃❌ 룅〯 ˧˦ 磊磥礌礧❌㠥礨壘櫑儡❌❌❌㒦❌蕾 룅〮 ˦ 酹類耒礧壘礌雷㵢攂 |} === ㅐ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:068 || ㄱ || 갱 ˧ 佳街皆偕湝階堦楷喈❌荄痎䕸稭祴該垓畡陔閡絯胲侅晐峐剴祴裓 갱〯 ˧˦ 解改胲❌陔閡 갱〮 ˦ 蓋蓋匃匄丐懈解繲廨解戒誡悈介界堺❌魪玠琾㠹价尬芥疥蚧❌䯰❌屆漑摡槪扢槪㧉剴 |- | 5:070 || ㅋ || 캥 ˧ 揩❌❌❌開 캥〯 ˧˦ 鍇楷愷凱豈鎧塏闓 캥〮 ˦ 磕礚愒❌渴嘅❌揩慨鎧闓欬咳愾 |- | 5:070 || ㆁ || ᅌᅢᆼ ˧ 厓崖涯漄睚倪皚❌凒敱 ᅌᅢᆼ〯 ˧˦ 騃 ᅌᅢᆼ〮 ˦ 艾❌㣻礙硋㝵儗閡 |- | 5:071 || ㄷ || 댕〮 ˦ 帶㿃蔕蹛遞 |- | 5:071 || ㅌ || 탱〮 ˦ 泰太太大忕㥭汰汏蠆𧀱蔕慸 |- | 5:071 || ㄸ || 땡〯 ˧˦ 廌豸豸 땡〮 ˦ 大軑釱汏 |- | 5:072 || ㄴ || 냉 ˧ 能 냉〯 ˧˦ 嬭㛋㚷乃迺鼐 냉〮 ˦ 柰耐𦓎能𣉘奈鼐 |- | 5:072 || ㅂ || 뱅〯 ˧˦ 擺❌捭 뱅〮 ˦ 貝狽伂沛拜扒敗 |- | 5:072 || ㅍ || 팽〮 ˦ 霈沛肺浿派湃㵒 |- | 5:073 || ㅃ || 뺑 ˧ 牌❌❌蠯螷❌排俳 뺑〯 ˧˦ 罷 뺑〮 ˦ 旆沛茷❌柭軷粺❌稗❌❌韛❌❌排糒敗唄 |- | 5:073 || ㅁ || 맹 ˧ 埋貍霾䨪 맹〯 ˧˦ 買 맹〮 ˦ 眛昧沬賣韎霾邁❌勱佅 |- | 5:074 || ㅈ || 쟁 ˧ 齋齊𩱳 쟁〯 ˧˦ 跐 쟁〮 ˦ 債責瘵祭 |- | 5:074 || ㅊ || 챙 ˧ 釵靫扠搋差䡨 챙〮 ˦ 蔡䌨縩瘥差衩𣲾 |- | 5:075 || ㅉ || 쨍 ˧ 柴祡豺犲儕 쨍〮 ˦ 眦❌疵眥砦柴 |- | 5:075 || ㅅ || 생 ˧ 簁篩 생〯 ˧˦ 灑洒躧蹝纚徙 생〮 ˦ 曬灑洒鎩𨦅殺閷煞 |- | 5:076 || ㆆ || ᅙᅢᆼ ˧ 娃哇❌挨唲 ᅙᅢᆼ〯 ˧˦ 矮㾨躷 ᅙᅢᆼ〮 ˦ 藹靄䨠馤壒❌濭隘阨搤噫❌呃嗄餲喝 |- | 5:076 || ㆅ || ᅘᅢᆼ ˧ 諧湝骸膎鞵鞋鮭 ᅘᅢᆼ〯 ˧˦ 蟹蟹獬❌鮭嶰澥解駭絯豥駴夥 ᅘᅢᆼ〮 ˦ 害妎邂解械❌薤❌瀣齘 |- | 5:077 || ㄹ || 랭〮 ˦ 賴瀨籟藾癩襰厲糲蠣賚 |} === ㅙ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:077 || ㄱ || 괭 ˧ 乖❌媧緺騧❌蝸 괭〯 ˧˦ 掛 괭〮 ˦ 卦掛挂絓罣詿怪怪壞夬澮獪㹟狤 |- | 5:078 || ㅋ || 쾡 ˧ ❌華䦱 쾡〮 ˦ 蒯❌蕢塊喟嘳㕟快噲 |- | 5:078 || ㆁ || ᅌᅫᆼ〮 ˦ 聵❌❌ |- | 5:078 || ㅊ || 쵕〮 ˦ 嘬 |- | 5:078 || ㆆ || ᅙᅫᆼ ˧ 蛙鼃洼 |- | 5:078 || ㆅ || ᅘᅫᆼ ˧ 懷櫰槐褢❌❌淮 ᅘᅫᆼ〮 ˦ 壞畵罫繣澅絓話❌躗吳 |} === ㅟ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:079 || ㄱ || 귕 ˧ 嬀潙佹龜騩歸 귕〯 ˧˦ 軌軌軌匭氿漸宄❌晷簋詭垝陒佹❌祪䤥庪庋攱傀鬼 귕〮 ˦ 媿愧❌聭騩攰庪貴瞶劌劂❌撅橛蹶❌❌鱖鱥 |- | 5:080 || ㅋ || 큉 ˧ 虧𧇾𩏣蘬巋 큉〯 ˧˦ 巋蘬 큉〮 ˦ 喟嘳㕟䙡巋缺 |- | 5:081 || ㄲ || 뀡 ˧ 逵㙺夔犪騤戣鍨頯頄馗 뀡〯 ˧˦ 跪 뀡〮 ˦ 匱鐀櫃饋歸蕢䕚簣籄㙺 |- | 5:081 || ㆁ || ᅌᅱᆼ ˧ 危峗峞帷爲韋幃褘湋違闈圍巍嵬犩❌ ᅌᅱᆼ〯 ˧˦ 頠姽峗瓦韙偉瑋煒颹暐韡❌葦 ᅌᅱᆼ〮 ˦ 僞胃❌謂渭蝟猬❌㥜媦緯圍彙❌❌魏犩❌衛熭❌❌轊䡺❌❌彘㻰 |- | 5:082 || ㄷ || 뒹〮 ˦ 轛 |- | 5:082 || ㆆ || ᅙᅱᆼ ˧ 逶倭䴧蜲痿萎委威葳蝛 ᅙᅱᆼ〯 ˧˦ 委骩磈嵬 ᅙᅱᆼ〮 ˦ 餧❌委骩尉慰蔚霨罻畏威 |- | 5:083 || ㅎ || 휭 ˧ 麾戲摩撝暉煇煒輝揮楎椲翬❌褘徽幑㫎 휭〯 ˧˦ 毁毁燬䅏❌毇❌烜䃣蟲虺蘬❌❌燬卉 휭〮 ˦ 毁諱卉 |- | 5:084 || ㅇ || 윙〯 ˧˦ 蔿蘤䦱❌❌ 윙〮 ˦ 位爲 |} === ㆌ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 5:084 || ㄱ || ᄀᆔᆼ ˧ 規槼摫瞡❌雉鳺 ᄀᆔᆼ〯 ˧˦ 癸湀 ᄀᆔᆼ〮 ˦ 季 |- | 5:084 || ㅋ || ᄏᆔᆼ ˧ 闚窺 ᄏᆔᆼ〯 ˧˦ 跬頃蹞窺❌頍頯 |- | 5:085 || ㄲ || ᄁᆔᆼ ˧ 葵楑鄈 ᄁᆔᆼ〯 ˧˦ 揆 ᄁᆔᆼ〮 ˦ 悸 |- | 5:085 || ㄷ || ᄃᆔᆼ ˧ 腄追䨨 |- | 5:085 || ㄸ || ᄄᆔᆼ ˧ 椎桘鎚錘槌甀魋椎鬌 ᄄᆔᆼ〮 ˦ 縋䋘槌膇㾽硾倕磓墜錘腄甀墜隊隧❌懟譵 |- | 5:086 || ㄴ || ᄂᆔᆼ〮 ˦ 諉 |- | 5:086 || ㅈ || ᄌᆔᆼ ˧ 劑檇㰎崔墔隹崒❌厜隹萑鵻騅錐 ᄌᆔᆼ〯 ˧˦ 觜❌嘴❌嶉捶棰錘箠菙 ᄌᆔᆼ〮 ˦ 醉檇雋惴諈錘 |- | 5:087 || ㅊ || ᄎᆔᆼ ˧ 吹龡炊推蓷衰 ᄎᆔᆼ〯 ˧˦ 趡揣 ᄎᆔᆼ〮 ˦ 翠臎吹䶴龡出 |- | 5:087 || ㅉ || ᄍᆔᆼ〮 ˦ 萃瘁悴顇踤 |- | 5:087 || ㅅ || ᄉᆔᆼ ˧ 綏浽荽荾䒘雖睢濉衰榱 ᄉᆔᆼ〯 ˧˦ 髓髓❌䯝䯝❌膸瀡靃❌巂嶲水準 ᄉᆔᆼ〮 ˦ 邃粹睟誶祟帥帨率 |- | 5:087 || ㅆ || ᄊᆔᆼ ˧ 隨隋遺垂陲倕❌腄誰唯譙脽 ᄊᆔᆼ〯 ˧˦ 菙❌ ᄊᆔᆼ〮 ˦ 遂燧❌鐩澻㴚襚❌璲繸❌❌隧❌隊旞❌❌彗篲穗❌穟瑞睡倕 |- | 5:089 || ㆆ || ᅙᆔᆼ〮 ˦ 恚烓 |- | 5:089 || ㅎ || ᄒᆔᆼ ˧ 墮𨼰𡐦隳隋綏觽䥴䥴睢倠 ᄒᆔᆼ〮 ˦ 睢隋綏墮挼侐 |- | 5:090 || ㅇ || ᄋᆔᆼ ˧ 惟維唯濰遺壝蠵❌❌觿 ᄋᆔᆼ〯 ˧˦ 洧鮪痏䔺❌芛芟唯踓趡壝 ᄋᆔᆼ〮 ˦ 遺❌壝瞶蜼 |- | 5:090 || ㄹ || ᄅᆔᆼ ˧ 羸纍縲累欙樏㹎❌虆藟❌蘲蔂絫 ᄅᆔᆼ〯 ˧˦ 壘藟虆蘽櫐讄❌❌❌礧❌㠥❌礨礨❌蠝❌累樏誄蜼猚 ᄅᆔᆼ〮 ˦ 類禷蘱率淚累絫 |- | 5:092 || ㅿ || ᅀᆔᆼ ˧ 甤蕤苼痿緌綏䅑䅗桵㮃 ᅀᆔᆼ〯 ˧˦ 繠橤蘂❌蕊 |} === ㅖ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:008 || ㄱ || 곙 ˧ 雞稽𮃛笄䈕枅𣓖卟乩 곙〮 ˦ 罽𦇧𣯅𦆡𣽄猘狾計繼㡭繫毄係❌髻結𨜒薊 |- | 6:008 || ㅋ || 콍 ˧ 谿溪磎嵠鸂䳶 콍〯 ˧˦ 啓棨𢧊綮启稽 콍〮 ˦ 愒憇𢠾偈❌揭䔾甈契栔鍥㓶挈 |- | 6:009 || ㄲ || 꼥〮 ˦ 偈碣嵑 |- | 6:009 || ㆁ || ᅌᅨᆼ〯 ˧˦ 掜 ᅌᅨᆼ〮 ˦ ❌甈乂刈㣻艾 |- | 6:010 || ㄷ || 뎽 ˧ 氐互低仾詆呧柢羝牴隄䧑堤鞮䩚磾 뎽〯 ˧˦ 邸䣌柢詆呧阺坻弤抵掋牴觝軝疧疷底㡳氐提堤底 뎽〮 ˦ 帝諦諟𧫚締蔕䗖柢泜氐疐嚔㗣 |- | 6:011 || ㅌ || 톙 ˧ 梯鷈鷉鵜𩀗 톙〯 ˧˦ 體軆躰涕緹紙醍 톙〮 ˦ 替暜朁涕洟鬀鬄剃殢揥楴𧝐褅裼薙雉𡲕傺袲 |- | 6:012 || ㄸ || 똉 ˧ 題提媞偍姼禔褆緹鞮睼瑅騠踶鶗嗁啼諦㖒㖷渧蹏蹄締綈䬾稊𦯔稺䄺𧀾銻𧋘鵜鴺罤䨑荑梯鮧鯷桋鴺㡗折 똉〯 ˧˦ 弟悌娣遞递𮞏 똉〮 ˦ 第弟悌娣睇眱珶遞迭递𮞏遰褅締杕軑釱踶提題鬄髢錫鶗鷤逮棣滯㿃䐭彘 |- | 6:014 || ㄴ || 녱 ˧ 泥埿臡 녱〯 ˧˦ 禰祢檷鑈鈮濔瀰薾爾泥 녱〮 ˦ 泥 |- | 6:015 || ㅂ || 볭 ˧ 篦豍❌㡙鎞❌狴❌ 볭〮 ˦ 閉箄嬖蔽草弊弊鷩鄨廢癈祓苃茀 |- | 6:015 || ㅍ || 폥 ˧ ❌批錍鈚漂 폥〮 ˦ 媲睤❌䁹辟壀埤僻濞淠潎❌肺杮 |- | 6:016 || ㅃ || 뼹 ˧ 鼙鞞𧯿椑㼰膍 뼹〯 ˧˦ 陛陛梐狴陛髀䯗䏶❌脾肶 뼹〮 ˦ 弊獘斃弊幣❌❌薜蘗❌吠犻❌茷 |- | 6:017 || ㅁ || 몡 ˧ 迷麛麑❌❌ 몡〯 ˧˦ 米眯䋛❌洣 몡〮 ˦ 袂 |- | 6:017 || ㅈ || 졩 ˧ 齎賷韲齏𦦏虀懠𢥎擠躋隮齊𨹷𢁃 졩〯 ˧˦ 濟泲㧗 졩〮 ˦ 霽擠濟𩯶祭際穄制製䱥鰶㫼晣晢淛浙折 |- | 6:018 || ㅊ || 촁 ˧ 妻萋霋凄淒悽緀 촁〯 ˧˦ 泚玼萋緀 촁〮 ˦ 切砌墄𥉻妻掣𢳅摯觢挈懘滯慸 |- | 6:018 || ㅉ || 쪵 ˧ 齊臍蠐 쪵〯 ˧˦ 薺齊癠鮆鱭 쪵〮 ˦ 嚌懠穧𨣧劑齊薺癠齌眥眦 |- | 6:019 || ㅅ || 솅 ˧ 西栖棲犀屖遟凘澌嘶撕 솅〯 ˧˦ 洗灑 솅〮 ˦ 細壻婿栖世貰勢埶殺閷 |- | 6:020 || ㅆ || 쏑 ˧ 栘 쏑〮 ˦ 誓逝遰遞筮簭噬澨遾忕 |- | 6:020 || ㆆ || ᅙᅨᆼ ˧ 鷖繄䃜瑿㙠黳 ᅙᅨᆼ〮 ˦ 瘞❌餲医瞖繄翳蘙嫕瘱曀㙪㙠殪縊 |- | 6:020 || ㅎ || 혱 ˧ 醯❌䤈橀 |- | 6:021 || ㆅ || ᅘᅨᆼ ˧ 兮❌奚㜎傒蹊❌騱貕豀謑鼷嵇 ᅘᅨᆼ〯 ˧˦ 徯諐蹊❌傒謑謑奚 ᅘᅨᆼ〮 ˦ 系繫係結禊妎盻 |- | 6:021 || ㅇ || 옝 ˧ 倪齯鯢蜺䘽輗❌棿麑猊猊霓兒 옝〮 ˦ 曳洩❌袣❌泄呭詍㖂䛖枻栧抴跇裔❌㵝㵩㳿㵩勩詣栺睨倪堄帠羿❌寱藝蓺❌槸褹袂囈 |- | 6:022 || ㄹ || 롕 ˧ 黎❌❌犁黧藜蔾瓈邌梨驪鸝蠡 롕〯 ˧˦ 禮澧醴鱧蠡䗍劙盠欐 롕〮 ˦ 麗䕻儷儷❌欐攦戾唳淚綟棙悷❌蜧茘❌蛠❌❌隷隷劙蠫❌㓯盭沴離例列栵迾裂厲厲礪❌❌糲爄勵蠣❌癘❌㾐❌ |} === ㆋ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:024 || ㄱ || ᄀᆒᆼ ˧ 圭窐❌❌❌閨袿邽蠲 ᄀᆒᆼ〮 ˦ 桂筀䳏 |- | 6:024 || ㅋ || ᄏᆒᆼ ˧ 睽聧奎暌刲㨒 |- | 6:025 || ㄷ || ᄃᆒᆼ〮 ˦ 綴餟醊腏畷錣 |- | 6:025 || ㅈ || ᄌᆒᆼ〮 ˦ 蕝蕞贅 |- | 6:025 || ㅊ || ᄎᆒᆼ〮 ˦ 脃膬毳橇竁喙 |- | 6:025 || ㅅ || ᄉᆒᆼ〮 ˦ 歲繐𦅵稅帨帥蛻涗裞䬽䥴說 |- | 6:025 || ㅆ || ᄊᆒᆼ〮 ˦ 篲 |- | 6:025 || ㆆ || ᅙᆒᆼ ˧ 烓❌ ᅙᆒᆼ〮 ˦ 穢薉濊獩 |- | 6:026 || ㅎ || ᄒᆒᆼ〮 ˦ 嘒嚖❌暳噦喙𣨶𤸁𠿔顪噦翽 |- | 6:026 || ㆅ || ᅘᆒᆼ ˧ 攜㩗携觿鑴蠵酅❌巂畦 ᅘᆒᆼ〮 ˦ 慧轊惠蕙譓憓 |- | 6:026 || ㅇ || ᄋᆒᆼ〮 ˦ 叡睿銳梲 |- | 6:027 || ㅿ || ᅀᆒᆼ〮 ˦ 汭內枘芮蜹 |} === ㅗ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:027 || ㄱ || 공 ˧ 孤❌觚❌柧軱呱❌箛䉉苽菰罛❌姑酤沽蛄鴣䧸辜❌❌橭❌盬夃 공〯 ˧˦ 古詁故估牯沽酤罟苦楛鼓❌瞽鼓股❌骰羖❌蠱監賈夃 공〮 ˦ 顧雇固凅錮鯝稒㧽痼故酤沽詁蠱 |- | 6:028 || ㅋ || 콩 ˧ 枯刳恗 콩〯 ˧˦ 苦䇢 콩〮 ˦ 庫袴絝胯跨❌ |- | 6:029 || ㆁ || ᅌᅩᆼ ˧ 吾浯梧鼯❌❌郚吳㻍珸鋘鋙 ᅌᅩᆼ〯 ˧˦ 五伍午迕仵旿 ᅌᅩᆼ〮 ˦ 誤悞悟晤旿晤捂梧忤牾午蘁逜寤❌遻❌迕俉愕 |- | 6:029 || ㄷ || 동 ˧ 都闍堵 동〯 ˧˦ 覩堵❌陼暏賭❌楮肚士 동〮 ˦ 妒妬奼㓃宅詫秅秺蠧螙❌斁❌睪 |- | 6:030 || ㅌ || 통 ˧ 稌 통〯 ˧˦ 土吐稌 통〮 ˦ 兎鵵菟吐 |- | 6:031 || ㄸ || 똥 ˧ 徒途墿峹𡷣荼𣘻余涂捈駼鵌鷋䅷䔑塗屠瘏菟𧈋檡兎圖鍍 똥〯 ˧˦ 杜荰土赭𢾅剫 똥〮 ˦ 度渡𣳥鍍塗 |- | 6:031 || ㄴ || 농 ˧ 奴㚢仅孥帑駑笯砮 농〯 ˧˦ 怒弩砮努 농〮 ˦ 笯怒㣽𢘂 |- | 6:032 || ㅂ || 봉 ˧ 逋餔哺誧晡 봉〯 ˧˦ 補䋠❌圃甫❌譜諩 봉〮 ˦ 布尃抪圃 |- | 6:032 || ㅍ || 퐁 ˧ 鋪痡鯆❌❌抪 퐁〯 ˧˦ 普浦誧溥 퐁〮 ˦ 怖鋪誧 |- | 6:033 || ㅃ || 뽕 ˧ 蒲莆蒱酺匍扶䒀瓿 뽕〯 ˧˦ 簿部蔀 뽕〮 ˦ 步❌荹捕哺餔䊇❌酺脯鞴 |- | 6:033 || ㅁ || 몽 ˧ 模橅❌❌謨謩嫫❌❌膜摹摸撫墓母 몽〯 ˧˦ 姥莽茻❌姆 몽〮 ˦ 暮慕募墓謨慔 |- | 6:034 || ㅈ || 종 ˧ 租蒩菹𦼬𧂚𧀽𦵔𦯓苴 종〯 ˧˦ 祖珇駔組阻岨俎柤𤕲齟詛 종〮 ˦ 作詛𥛜謯作 |- | 6:035 || ㅊ || 총 ˧ 麤麆粗觕初 총〯 ˧˦ 楚礎濋憷❌ 총〮 ˦ 措厝錯醋楚 |- | 6:035 || ㅉ || 쫑 ˧ 徂且鉏鋤助耡 쫑〯 ˧˦ 粗麆𧇿 쫑〮 ˦ 祚胙飵阼葄助耡鉏莇 |- | 6:036 || ㅅ || 송 ˧ 蘇穌㢝酥𦣑𨢭蔬疏疎𤕠梳𣐌𣓜疋綀釃斯 송〯 ˧˦ 所𢨷䝪糈 송〮 ˦ 素愫榡傃嗉訴𧪜愬㴑遡溯泝塑塐疏𥿇 |- | 6:037 || ㆆ || ᅙᅩᆼ ˧ 烏於杇圬釫❌汙汚於嗚惡 ᅙᅩᆼ〯 ˧˦ 塢䃖埡瑦鄔 ᅙᅩᆼ〮 ˦ 汙洿盓❌惡䛩䜑堊嗚 |- | 6:037 || ㅎ || 홍 ˧ 呼虖謼嘑滹❌淲❌❌泘膴幠芋 홍〯 ˧˦ 虎琥滸滹許 홍〮 ˦ 謼嘑呼戽 |- | 6:038 || ㆅ || ᅘᅩᆼ ˧ 胡❌𠴱箶❌湖瑚餬❌䭌醐❌糊粘❌❌❌䉿❌鶘乎虖壷弧狐❌瓠葫 ᅘᅩᆼ〯 ˧˦ 戶昈雇❌扈❌怙❌岵祜酤楛鄠❌下芐芦嫭嫮橭 ᅘᅩᆼ〮 ˦ 護濩頀䪝穫互枑冱瓠嫮嫭涸 |- | 6:039 || ㄹ || 롱 ˧ 盧蘆籚廬鑢爐櫨鱸壚艫轤瓐黸獹瀘矑纑顱髗❌❌玈❌❌ 롱〯 ˧˦ 魯櫓櫓㯭艣虜擄鹵滷塷瀂❌ 롱〮 ˦ 路璐露簬❌鷺❌鸕鴼潞輅賂 |} === ㅏ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:041 || ㄱ || 강 ˧ 歌謌❌戨❌牁哥柯鴚舸渮荷渮嘉佳加笳茄鴐駕珈耞跏迦痂枷葭麚豭猳家 강〯 ˧˦ 哿笴舸菏荷❌賈檟榎夏假徦嘏椵斝斝 강〮 ˦ 箇個介駕架榢枷稼嫁幏假叚下斝斝價賈 |- | 6:042 || ㅋ || 캉 ˧ 珂砢軻呿 캉〯 ˧˦ 可岢軻坷 캉〮 ˦ 軻坷髂䯊❌ |- | 6:043 || ㆁ || ᅌᅡᆼ ˧ 莪峨峨我娥俄哦睋蛾䖸鵝❌䳘鵞牙芽枒㧎衙涯厓 ᅌᅡᆼ〯 ˧˦ 我騀硪峨雅鴉庌牙疋 ᅌᅡᆼ〮 ˦ 餓訝迓御輅衙庌砑牙 |- | 6:044 || ㄷ || 당 ˧ 多多奓 당〯 ˧˦ 嚲癉哆打 당〮 ˦ 癉憚咤喥䖳㓃宅❌奼哆奓 |- | 6:044 || ㅌ || 탕 ˧ 佗他拕拖扡蛇詑訑 탕〯 ˧˦ 姹 탕〮 ˦ 詫❌咤侘差 |- | 6:045 || ㄸ || 땅 ˧ 駝駞它沱沲池陀岮陁阤跎❌佗紽酡鮀迤池鼉鱓鱣驒馱秅茶𣘻涂塗 땅〯 ˧˦ 拕拖扡佗柁舵䑨杕袉沱❌❌沲阤陀陊爹 땅〮 ˦ 馱他大 |- | 6:046 || ㄴ || 낭 ˧ 那難單儺拏挐笯 낭〯 ˧˦ 娜那袲橠難❌旎儺 낭〮 ˦ 柰那哪㖠 |- | 6:047 || ㅂ || 방 ˧ 波碆番僠嶓巴笆芭豝❌羓鈀 방〯 ˧˦ 跛❌簸播把 방〮 ˦ 播譒磻簸霸伯灞壩䃻靶弝杷欛 |- | 6:048 || ㅍ || 팡 ˧ 頗❌坡岥❌陂❌玻葩苩舥芭 팡〯 ˧˦ 頗叵 팡〮 ˦ 破帊帕袙怕❌❌ |- | 6:048 || ㅃ || 빵 ˧ 婆鄱番皤❌番杷爬把琶 빵〮 ˦ 縛杷䆉❌罷 |- | 6:049 || ㅁ || 망 ˧ 摩擵磨❌麼❌䯢魔劘攠麻菻蟆䗫蟇 망〯 ˧˦ 麼䯢馬 망〮 ˦ 磨禡貊伯榪罵䣕䣖鬕 |- | 6:049 || ㅈ || 장 ˧ 樝❌𣕈柤齟 장〯 ˧˦ 左𡯛鮓䱹𩽫苴苲 장〮 ˦ 左佐𡯛作詐醡笮苲榨𨣮𨢦溠咋 |- | 6:050 || ㅊ || 창 ˧ 蹉嵯磋瑳搓差叉杈靫扠差艖舣鎈 창〯 ˧˦ 瑳 창〮 ˦ 磋蹉差汊衩 |- | 6:051 || ㅉ || 짱 ˧ 醝鹺艖𦪸䑘㽨瘥𣩈嵳𥰭齹䣜鄼 |- | 6:051 || ㅅ || 상 ˧ 娑挱莎莏挲沙䤬𨪍桫髿𣯌犠獻戲傞𠈱沙𣲡砂紗𦀟鯊㲚髿硰 상〯 ˧˦ 娑灑洒葰傻 상〮 ˦ 些娑逤嗄𣣺沙 |- | 6:052 || ㅆ || 쌍 ˧ 槎楂苴 쌍〯 ˧˦ 槎𠞊柞 쌍〮 ˦ 乍䄍蓌 |- | 6:053 || ㆆ || ᅙᅡᆼ ˧ 阿娿痾䋪䋍鴉鴉丫啞亞 ᅙᅡᆼ〯 ˧˦ 娿婀妸❌阿猗啞瘂❌ ᅙᅡᆼ〮 ˦ 亞惡啞稏婭 |- | 6:053 || ㅎ || 항 ˧ 訶苛何呵㰤呀谺岈❌鰕 항〯 ˧˦ 㰤閜 항〮 ˦ 呵罅㙤釁❌❌嚇赫哧謑 |- | 6:054 || ㆅ || ᅘᅡᆼ ˧ 何荷河苛遐徦假蕸霞瑕鍜碬蝦鰕騢假赮 ᅘᅡᆼ〯 ˧˦ 荷何❌抲苛下夏廈 ᅘᅡᆼ〮 ˦ 賀❌荷何暇假嘉下芐夏 |- | 6:055 || ㄹ || 랑 ˧ 羅蘿籮❌鑼欏囉饠 랑〯 ˧˦ 砢❌㦬❌攞邏 |} === ㅑ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:055 || ㄱ || 걍 ˧ 迦 |- | 6:055 || ㅋ || 컁 ˧ 呿 |- | 6:055 || ㄲ || 꺙 ˧ 伽茄 |- | 6:055 || ㅈ || 쟝 ˧ 嗟罝𦋽遮庶 쟝〯 ˧˦ 姐她媎者赭堵 쟝〮 ˦ 借藉唶柘䂞蔗𤯈𤯋鷓蟅䗪炙 |- | 6:056 || ㅊ || 챵 ˧ 車 챵〯 ˧˦ 且奲撦䰩奓哆拸䞣 |- | 6:056 || ㅉ || 쨩〯 ˧˦ 抯飷 쨩〮 ˦ 藉蒩耤 |- | 6:057 || ㅅ || 샹 ˧ 些𡭟奢賖畬 샹〯 ˧˦ 寫𣞐瀉捨舍 샹〮 ˦ 卸瀉舍赦厙 |- | 6:057 || ㅆ || 썅 ˧ 邪耶斜䔑𦳃闍堵蛇虵鉈 썅〯 ˧˦ 䄬社 썅〮 ˦ 謝榭㴬射䠶麝貰 |- | 6:058 || ㅇ || 양 ˧ 邪釾鋣鎁枒椰椰耶爺斜 양〯 ˧˦ 野野也冶蠱 양〮 ˦ 夜射 |- | 6:058 || ㅿ || ᅀᅣᆼ〯 ˧˦ 惹❌若 |} === ㅘ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:058 || ㄱ || 광 ˧ 戈過撾渦❌鍋鈛❌瓜媧騧❌蝸緺 광〯 ˧˦ 果菓惈❌裹蜾裸寡❌另剮 광〮 ˦ 過裹燴 |- | 6:059 || ㅋ || 쾅 ˧ 科蝌窠薖簻誇侉胯跨姱恗夸❌荂華 쾅〯 ˧˦ 顆堁果骻胯跨㡁袴銙❌錁❌夸 쾅〮 ˦ 課堁跨❌夸胯袴❌ |- | 6:060 || ㄲ || 꽝 ˧ 瘸 |- | 6:060 || ㆁ || ᅌᅪᆼ ˧ 吪❌囮繇鈋譌訛 ᅌᅪᆼ〯 ˧˦ 瓦 ᅌᅪᆼ〮 ˦ 臥 |- | 6:060 || ㄷ || 돵 ˧ 檛薖撾 돵〯 ˧˦ 朶揣挅捶㪜崜埵𥠄鬌 |- | 6:061 || ㅌ || 퇑 ˧ 詑 퇑〯 ˧˦ 妥綏𢼻隋橢䲊墮媠 퇑〮 ˦ 唾涶佗扡拕拖大 |- | 6:061 || ㄸ || 뙁〯 ˧˦ 惰嶞墮墯隓𡐦垜𨹃䅜 뙁〮 ˦ 惰憜媠墮𢞑𢢠 |- | 6:062 || ㄴ || 놩 ˧ 捼挼 놩〮 ˦ 愞懦偄𦓏稬糯𤲬堧壖 |- | 6:062 || ㅈ || 좡 ˧ 髽 좡〮 ˦ 挫蓌夎䟶 |- | 6:062 || ㅊ || 촹〯 ˧˦ 脞 촹〮 ˦ 剉挫莝摧 |- | 6:062 || ㅉ || 쫭 ˧ 矬痤 쫭〯 ˧˦ 坐 쫭〮 ˦ 坐座 |- | 6:063 || ㅅ || 솽 ˧ 蓑莎梭𥭟唆 솽〯 ˧˦ 鎖鏁瑣璅䵀葰 |- | 6:063 || ㆆ || ᅙᅪᆼ ˧ 倭踒渦窩窊窳溛窪窐洼❌哇娃蛙䵷汙 ᅙᅪᆼ〯 ˧˦ 婐果 ᅙᅪᆼ〮 ˦ 涴汙堁 |- | 6:064 || ㅎ || 황 ˧ 鞾靴❌❌㞜花華❌ 황〯 ˧˦ 火 황〮 ˦ 貨化 |- | 6:064 || ㆅ || ᅘᅪᆼ ˧ 和龢鉌禾華崋譁嘩驊❌鋘釫鏵 ᅘᅪᆼ〯 ˧˦ 禍❌輠❌夥❌髁踝裸裸觟 ᅘᅪᆼ〮 ˦ 和華樺檴嬅摦擭獲鱯㕦 |- | 6:065 || ㄹ || 뢍 ˧ 鸁䯁騾臝蠃螺蠡蝸❌❌鏍虆蔂❌❌覼 뢍〯 ˧˦ 裸裸果臝儽蠃蓏❌蠡❌瘰攭 뢍〮 ˦ 邏摞 |} === ㅜ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:066 || ㄱ || 궁 ˧ 拘佝句❌跔駒驕痀俱㪺❌仇捄 궁〯 ˧˦ 矩榘距句拒枸蒟萬楀踽偊鄅 궁〮 ˦ 屨鞻句絇蒟瞿懼 |- | 6:067 || ㅋ || 쿵 ˧ 區驅毆敺❌歐嶇軀❌鮬摳 쿵〯 ˧˦ 齲踽 쿵〮 ˦ 驅 |- | 6:067 || ㄲ || 꿍 ˧ 劬朐❌絇句❌❌䋧軥枸鼩翑句臞癯躣躩忂灈懼戵鑺欋衢瞿鸜 꿍〯 ˧˦ 寠窶 꿍〮 ˦ 懼瞿具❌❌ |- | 6:068 || ㆁ || ᅌᅮᆼ ˧ 虞❌澞鸆麌娛禺愚隅嵎髃腢喁齵堣鍝于竽𥫡迂盂❌杅❌玗釪汙邘❌雩❌謣❌ ᅌᅮᆼ〯 ˧˦ 麌❌俁㒁羽禹䨞萭瑀偊宇㝢㡰❌雨 ᅌᅮᆼ〮 ˦ 遇寓庽禺虞芌芋𩁹吁羽雨 |- | 6:070 || ㅂ || 붕 ˧ 膚❌夫玞扶䄮鈇柎不❌枎跗趺附❌枹 붕〯 ˧˦ 甫父莆黼❌脯俌簠❌府腑俯斧鈇 붕〮 ˦ 付傅❌賦富 |- | 6:071 || ㅍ || 풍 ˧ 敷傳鋪尃溥懯孚莩罦䍖❌稃粰❌❌桴俘郛垺𡎽苻荴紨泭柎❌怤荂❌旉華麩麱䴸鄜 풍〯 ˧˦ 撫❌拊柎弣 풍〮 ˦ 赴訃仆踣掊䞳趏簠副報 |- | 6:072 || ㅃ || 뿡 ˧ 扶蚨颫夫❌芙符苻❌鳧瓿 뿡〯 ˧˦ 父㕮駁輔䩉❌鬴釜❌秿❌滏腐焤 뿡〮 ˦ 附胕柎付駙䮛鮒❌柎坿跗傅賻負負萯偩婦 |- | 6:073 || ㅁ || 뭉 ˧ 無亡蕪䍢❌璑毋巫誣 뭉〯 ˧˦ 武鵡母碔珷璑舞儛廡❌膴嫵斌憮㒇嘸甒❌❌橆蕪侮侮務䍙堥 뭉〮 ˦ 務婺瞀騖鶩霧 |- | 6:074 || ㅊ || 충 ˧ 芻䅳 |- | 6:074 || ㅉ || 쭝 ˧ 雛䅳媰 |- | 6:074 || ㅅ || 숭 ˧ 毹毺㲣𡨙氀 숭〮 ˦ 數捒 |- | 6:074 || ㆆ || ᅙᅮᆼ ˧ 紆汙迂盓陓 ᅙᅮᆼ〯 ˧˦ 傴痀❌嫗噢 ᅙᅮᆼ〮 ˦ 嫗饇歐燠噢 |- | 6:075 || ㅎ || 훙 ˧ 訏吁盱❌芋❌旴晇冔❌昫煦蓲姁喣嘔呴謳 훙〯 ˧˦ 詡栩珝訏冔❌姁昫喣煦煦膴咻 훙〮 ˦ 煦昫呴欨咻休䣱酗 |- | 6:076 || ㄹ || 룽 ˧ 慺膢褸漊鏤氀❌蔞𦭯婁 룽〯 ˧˦ 縷僂軁漊嶁㟺褸簍蔞婁 룽〮 ˦ 屢婁僂 |} === ㅠ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:077 || ㄷ || 듕 ˧ 株誅蛛跦邾禂 듕〯 ˧˦ 𪐴拄柱 듕〮 ˦ 駐軴柱住𨙦𩦛 |- | 6:077 || ㅌ || 튱 ˧ 貙 |- | 6:077 || ㄸ || 뜡 ˧ 廚躕裯幮幬趎 뜡〯 ˧˦ 柱 뜡〮 ˦ 住 |- | 6:078 || ㅈ || 즁 ˧ 諏𧩻娵朱珠侏咮祩袾 즁〯 ˧˦ 主宔砫袾麈炷枓斗 즁〮 ˦ 足注屬主註炷鉒䪒澍霔咮𠰍鑄馵 |- | 6:078 || ㅊ || 츙 ˧ 趨𨃘趣騶取樞姝 츙〯 ˧˦ 取 츙〮 ˦ 娶趣趨 |- | 6:079 || ㅉ || 쯍〯 ˧˦ 聚 쯍〮 ˦ 聚鄹𡒍㝡 |- | 6:079 || ㅅ || 슝 ˧ 須𩓣𥪙𥪥䇕𡡓鬚需繻緰㡏𦅎𪋯輸隃鄃 슝〯 ˧˦ 數籔簍藪 슝〮 ˦ 戍隃輸束 |- | 6:080 || ㅆ || 쓩 ˧ 殊銖洙㼡茱殳杸 쓩〯 ˧˦ 豎𠐊侸裋𧞫樹 쓩〮 ˦ 樹澍裋𧞫 |- | 6:080 || ㅇ || 융 ˧ 兪逾踰𧼯窬瘉瘐𨵦渝楡揄𦩞羭蝓𧍳堬瑜牏褕喩愉婾愈睮覦歈㼶臾㬰諛楰腴㥚萸 융〯 ˧˦ 庾㢏㔱楰㥚斞斔愉瘐瘉臾愈瘉癒貐窳寙 융〮 ˦ 裕䘱欲慾籲諭喩覦兪瘉 |- | 6:082 || ㅿ || ᅀᅲᆼ ˧ 儒𠍶嚅吺咮懦愞濡𣽉醹襦𧝄 ᅀᅲᆼ〯 ˧˦ 乳醹㳶 ᅀᅲᆼ〮 ˦ 孺𡦗乳 |} === ㅓ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:083 || ㄱ || 겅 ˧ 居㞐琚❌裾据椐鶋❌車 겅〯 ˧˦ 擧莒筥❌籧❌柜欅弆 겅〮 ˦ 據鐻踞倨裾鋸據居擧 |- | 6:083 || ㅋ || 컹 ˧ 墟丘嶇㠊袪祛佉胠阹呿抾魼鱋去 컹〯 ˧˦ 去麮胠 컹〮 ˦ 去㰦呿胠 |- | 6:084 || ㄲ || 껑 ˧ 渠蕖❌❌❌磲❌蘧遽籧鐻❌璩璖❌醵䣰腒 껑〯 ˧˦ 巨鉅詎渠距❌❌拒歫蚷駏❌炬❌秬岠怇苣虡簴❌❌鐻 껑〮 ˦ 遽蘧懅醵䣰勮詎巨渠 |- | 6:085 || ㆁ || ᅌᅥᆼ ˧ 魚䁩❌䐳❌漁䱷䰻齬衙 ᅌᅥᆼ〯 ˧˦ 語齬鋙❌峿敔梧衙圄圉禦御蘌❌❌❌ ᅌᅥᆼ〮 ˦ 御御禦衙圉語䱷漁 |- | 6:086 || ㆆ || ᅙᅥᆼ ˧ 於淤唹 ᅙᅥᆼ〮 ˦ 飫饇❌❌棜淤❌瘀菸 |- | 6:086 || ㅎ || 헝 ˧ 虛虗歔噓吁喣呴驉❌魖 헝〯 ˧˦ 許 헝〮 ˦ 噓 |} === ㅕ === {| class="prettytable" cellpadding="0" cellspacing="0" border="1" ! scope="col" width="0%" | 권:쪽 ! scope="col" width="0%" | 첫 ! scope="col" width="100%" | 한자 |- | 6:087 || ㄷ || 뎡 ˧ 豬猪䐗櫫瀦潴 뎡〯 ˧˦ 貯𡪄著紵䘢宁 뎡〮 ˦ 著 |- | 6:087 || ㅌ || 텽 ˧ 攄捈樗櫖摴摢㻬 텽〯 ˧˦ 楮柠褚 텽〮 ˦ 絮 |- | 6:088 || ㄸ || 뗭 ˧ 除篨滁筡著躇屠儲宁 뗭〯 ˧˦ 宁佇竚紵𦂂羜泞眝坾杼拧䇡抒苧 뗭〮 ˦ 箸櫡躇著宁除 |- | 6:088 || ㄴ || 녕 ˧ 袽帤挐拏 녕〯 ˧˦ 女 녕〮 ˦ 女 |- | 6:089 || ㅈ || 졍 ˧ 苴蒩且蛆沮諸䃴蠩 졍〯 ˧˦ 苴疽䰞𩱰𤑨煮渚陼庶 졍〮 ˦ 怚姐𢚆沮苴翥䬡庶 |- | 6:089 || ㅊ || 쳥 ˧ 疽砠❌沮趄跙狙雎苴 쳥〯 ˧˦ 且杵処處 쳥〮 ˦ 覰䁦狙蜡處 |- | 6:090 || ㅉ || 쪙〯 ˧˦ 沮咀跙 |- | 6:090 || ㅅ || 셩 ˧ 胥諝㥠胥糈稰蝑湑書𦘠舒豫𪅰紓䋡悆忬瑹荼 셩〯 ˧˦ 諝醑湑稰胥暑黍鼠癙 셩〮 ˦ 絮恕庶 |- | 6:091 || ㅆ || 쎵 ˧ 徐邪蜍蠩 쎵〯 ˧˦ 敍漵㵰序䦽㘧茅𣏗杼藇𨣦鱮魣嶼緖紓墅野杼茅𣏗桃 쎵〮 ˦ 署𥌓暏 |- | 6:092 || ㅇ || 영 ˧ 余予畬❌畭蜍餘與歟欤懙❌㦛旟璵㼂譽鸒輿❌舁伃妤茅雓 영〯 ˧˦ 與❌歟予❌ 영〮 ˦ 豫舒余與鸒譽礜藇蕷穥歟輿預忬澦❌悆 |- | 6:093 || ㄹ || 령 ˧ 臚❌盧膚驢䮫廬慮藘閭櫚 령〯 ˧˦ 呂呂侶梠膂旅❌祣❌臚穭稆儢❌ 령〮 ˦ 慮爈❌䥨鋁❌濾勴錄窶 |- | 6:094 || ㅿ || ᅀᅧᆼ ˧ 如鴐茹絮洳 ᅀᅧᆼ〯 ˧˦ 汝籹❌女茹 ᅀᅧᆼ〮 ˦ 茹洳如 |} {{단원 안내|책=동국정운 색인|상위=동국정운 색인 |이전=끝소리 ㅱ |다음=끝소리 ㆁ}} [[분류:동국정운]] dguwtzng5b5m5hpz6py7mu6nlrpa1ho 하스켈 0 11856 45162 44654 2024-10-31T11:31:31Z Vpark45 8370 45162 wikitext text/x-wiki [[분류:책:하스켈]] [[분류:책장:하스켈 프로그래밍 언어]] [[File:Haskell-Logo.svg|250px|right|Haskell Logo]] 하스켈은 [[:w:ko:함수형 프로그래밍|함수형 프로그래밍]] 언어이다. 하스켈은 아래와 같은 특징이 있다. * 하스켈은 순수하다. 같은 인자를 넣은 함수는 항상 같은 결과가 나온다. * 하스켈은 [[:w:ko:느긋한 계산법|느긋하다]]. 필요할 때만 값을 평가한다. * 하스켈은 현대적인 기능을 갖춘 최첨단 타입 시스템을 제공한다. 타입클래스와 일반화된 대수적 데이터 타입 같은 기능이 있다. 순수 함수를 사용하면 코드 추론을 더 쉽게 할 수 있다. 고급 타입 시스템은 사소하거나 심각한 실수 잡는 것을 도와준다. 이 책의 목표는 독자에게 하스켈 프로그래밍 언어를 소개하는 것이다. 이 책의 범위는 아주 기초부터 고급 기능까지이고 일반적인 컴퓨터 프로그래밍도 다룬다. <!-- We urge seasoned programmers to be especially patient with this process. --> 독자에게 이미 익숙한 다른 프로그래밍 언어는 하스켈과 많이 다를 가능성이 크다. 다른 프로그래밍 언어에서 생긴 습관 때문에 독자가 하스켈 동작 과정을 이해하는 데 어려움을 겪을 수 있다. 하스켈은 단순하지만 어렵다. 함수형 프로그래머의 특별한 사고방식으로 세상 보는 법을 배우는 것은 새로운 세계로의 모험이다. 이 과정에서 얻는 지식은 특정 언어의 경계를 훨씬 넘어서는 가치를 제공한다. == 개요 == 이 책은 다음과 같이 크게 세 개의 절로 구성되어 있다. * 초급 과정 * 고급 과정 * 실용 하스켈 실용 사례를 다루는 마지막 절은 초급 과정 절만 알아도 이해할 수 있다. 어떤 점이 하스켈을 독특하고 다른 프로그래밍 언어와 다르게 만들까? 숙련된 프로그래머라면 [[/맛보기/]] 절을 통해 어떤 점이 하스켈을 그렇게 만드는지 빠르게 평가해보기 바란다. == 초급 과정 == 이 절에서는 하스켈 기초와 자주 쓰이는 라이브러리를 소개한다. 이 과정을 마치면 간단한 하스켈 프로그램을 작성할 수 있다. 대부분의 장에 연습 문제와 해답이 실려 있다. === 하스켈 기초 === * [[/준비하기/]] * [[/변수와 함수/]] 2v4rayjmi47jl209vholwg9p9avsas3 일반인을 위한 파이썬 지침서 0 11941 45172 44796 2024-10-31T11:49:36Z Vpark45 8370 /* 저작권 */ 45172 wikitext text/x-wiki == 저작권 == Copyright(c) 1999-2000 Josh Cogliati. 저작권과 허가 표시가 유지되는한, 그리고 배포자가 접수자에게 이 게시대로 재배포가 가능함을 고지하는 한, 누구라도 이 문서의 복사본을 접수한 그대로, 어떠한 형태로든지 복사본을 만들거나 배포할 수 있다. 또한 가장 마지막으로 그 문서를 변경한 사람이 확실하게 그 문서에 고지되어 있는한, 위에 게시한 조건 아래서, 이 문서를 변경한 버젼을 혹은 그 일부분을 배포하는 것도 가능하다. 이 지침서의 모든 파이썬의 예제 코드들은 공유 영역에 공헌되었다. 그러므로 여러분은 그 코드를 변경할 수도 있고 그리고 여러분이 원하는 라이선스 아래서 다시 라이선스를 줄 수도 있다. == 역자의 말 == Copyleft(t) 2001 johnsonj. * 번역기간: 2001. 7. 1. ~ 2001. 7. 10. * 버전: 수정중 * 역자: 전순재 johnsonj@hanmir.com 원문은 "non-programer's tutorial for python" 이다. 직역하면 «파이썬을 위한 비-프로그래머의 튜토리얼» 이다. 내용은 사용자와 대화하는투로 쓰여져서 본토인들은 쉽겠지만 라인대 라인으로 번역하는 역자로서는 상당히 어려웠다. 일단 언어가 전문용어가 아니라 일상용어이고 파이썬 언어(변수 명령어도 모두 영어임)와 일상어를 원저자가 마구 섞어 쓴 관계로 이해하기에 문제가 있을 수 있다. 그리고 처음에는 ‘-하세요’ 체로 정답게 번역하려고 하였으나 뒤로 갈수록 내용이 어려워져서 할 수 없이 ‘-하라’ 체로 바꾸었다. 능력의 한계를 느낀다. 지금 버전은 무조건 한글로 바꾼 버전이다. 문제가 많으므로 보시는 분들이 고쳐 주시길 바란다. 파이썬을 처음 배우는 사람에게 조금이라도 도움이 되길 바란다. 역자도 이 글을 이해하지 못한다. 하지만 영문보다야 한글로 따라가는게 편하리라 생각한다. 쓰레기를 올려서 머리를 혼란시킬 의도는 없으므로 비난하지 말아 주시길 바란다. 내딛는 발길을 두려워하며 johnsonj - 2001. 7. 10. == 목차 == * [[/서문/]] * [[/소개/]] * [[/Hello, World/]] * [[/누가 거기에 가지?/]] * [[/10까지 세어보기/]] * [[/결정/]] * [[/디버깅/]] * [[/함수 정의/]] * [[/리스트/]] * [[/For 회돌이/]] * [[/부울 표현식/]] * [[/사전/]] * [[/모듈 사용/]] * [[/리스트를 더 자세히/]] * [[/문자열의 복수/]] * [[/파일 입출력/]] * [[/불완전을 다루기(또는 에러처리법)/]] * [[/마지막/]] * [[/FAQ/]] * [[/이 문서에 대하여.../]] == 출처 == 이 책의 원문과 번역은 [http://jjc.freeshell.org/easytut/korean/easytut.html 이 사이트]에 실려 있다. jgrnn5gahep6hvjzimb7zofs1ott3aj 45173 45172 2024-10-31T11:50:59Z Vpark45 8370 /* 역자의 말 */ 45173 wikitext text/x-wiki == 저작권 == Copyright(c) 1999-2000 Josh Cogliati. 저작권과 허가 표시가 유지되는한, 그리고 배포자가 접수자에게 이 게시대로 재배포가 가능함을 고지하는 한, 누구라도 이 문서의 복사본을 접수한 그대로, 어떠한 형태로든지 복사본을 만들거나 배포할 수 있다. 또한 가장 마지막으로 그 문서를 변경한 사람이 확실하게 그 문서에 고지되어 있는한, 위에 게시한 조건 아래서, 이 문서를 변경한 버젼을 혹은 그 일부분을 배포하는 것도 가능하다. 이 지침서의 모든 파이썬의 예제 코드들은 공유 영역에 공헌되었다. 그러므로 여러분은 그 코드를 변경할 수도 있고 그리고 여러분이 원하는 라이선스 아래서 다시 라이선스를 줄 수도 있다. == 역자의 말 == Copyleft(t) 2001 johnsonj. * 번역기간: 2001. 7. 1. ~ 2001. 7. 10. * 버전: 수정중 * 역자: 전순재 johnsonj@hanmir.com 원문 제목은 “non-programer's tutorial for python”이다. 직역하면 «파이썬을 위한 비-프로그래머의 튜토리얼» 이다. 내용은 사용자와 대화하는투로 쓰여져서 본토인에게는 쉽겠지만 번역하는 역자로서는 상당히 어려웠다. 일단 언어가 전문 용어가 아니라 일상 용어이고 파이썬 언어(변수 명령어도 모두 영어임)와 일상어를 원저자가 마구 섞어 쓴 관계로 이해하기에 문제가 있을 수 있다. 그리고 처음에는 ‘-하세요’ 체로 정답게 번역하려고 하였으나 뒤로 갈수록 내용이 어려워져서 할 수 없이 ‘-하라’체로 바꾸었다. 능력의 한계를 느낀다. 지금 버전은 무조건 한글로 바꾼 버전이다. 문제가 많으므로 보시는 분들이 고쳐 주시길 바란다. 파이썬을 처음 배우는 사람에게 조금이라도 도움이 되길 바란다. 역자도 이 글을 이해하지 못한다. 하지만 영문보다야 한글로 따라가는 게 편하리라 생각한다. 쓰레기를 올려서 머리를 혼란시킬 의도는 없으므로 비난하지 말아 주시길 바란다. 내딛는 발길을 두려워하며 johnsonj - 2001. 7. 10. == 목차 == * [[/서문/]] * [[/소개/]] * [[/Hello, World/]] * [[/누가 거기에 가지?/]] * [[/10까지 세어보기/]] * [[/결정/]] * [[/디버깅/]] * [[/함수 정의/]] * [[/리스트/]] * [[/For 회돌이/]] * [[/부울 표현식/]] * [[/사전/]] * [[/모듈 사용/]] * [[/리스트를 더 자세히/]] * [[/문자열의 복수/]] * [[/파일 입출력/]] * [[/불완전을 다루기(또는 에러처리법)/]] * [[/마지막/]] * [[/FAQ/]] * [[/이 문서에 대하여.../]] == 출처 == 이 책의 원문과 번역은 [http://jjc.freeshell.org/easytut/korean/easytut.html 이 사이트]에 실려 있다. ie8qnaanqh00wlk7iekuib1l0b2sq8w 일반인을 위한 파이썬 지침서/서문 0 11942 45174 44772 2024-10-31T11:54:30Z Vpark45 8370 45174 wikitext text/x-wiki 일반인을 위한 파이썬 지침서는 파이썬 프로그래밍 언어에 대한 소개를 위하여 디자인된 지침서이다. 이 지침서는 프로그래밍 경험이 전혀 없는 사람을 위한 것이다. 여러분이 다른 언어로 프로그램 해 본 적이 있다면 귀도 반 로섬이 쓴 파이썬 지침서를 사용하기를 강력히 추천한다. 이 문서는 LaTeX, HTML, PDF, Postscript 등의 형태로 이용 가능하다. 여기로 가면 이러한 모든 형식을 볼 수 있다. 여러분이 질문이 있거나 덧붙이고자 하는 바가 있다면 jjc@iname.com로 나에게 연락을 취하라. 나는 이 문서에 관한 질문과 주석을 환영하는 바이다. 나는 될 수 있는한 최선을 다해서 어떠한 질문에 대해서도 응답하려고 노력할 것이다. 윈도우 설치 정보의 대부분을 작성해준 James A. Brown에게 감사드린다. 또한 본래의 지침서(일반인은 거의 사용할 수 없는 것이다.)에 관하여 불평을 해주고 또한 문서의 내용에 관하여 일일이 점검해준 Elizabeth Cogliati에게 감사드린다. 내가 빼먹었을 지도 모를 모든 분에게 감사드린다. 여기에 여러분이 유용하다고 생각할 만한 링크 몇 개를 나열한다. * [http://www.python.org/ 파이썬 홈페이지] * [http://www.python.org/doc/ 파이썬 문서] * [http://www.python.org/doc/current/tut/tut.html 프로그래머를 위한 파이썬 지침서] * [http://www.honors.montana.edu/~jjc/easytut/ LaTeX, PDF, and Postscript, 그리고 Zip 버전] 이 책을 Elizabeth Cogliati에게 바친다. 20oet3m3e2bpxg8l0hmzlram1ufp7i5 45175 45174 2024-10-31T11:57:25Z Vpark45 8370 45175 wikitext text/x-wiki 파이썬 2를 쓰지 말고 파이썬 3를 써라. 파이썬 2는 개발이 중단되었고 파이썬 3로 대체되었다. 파이썬을 처음 배운다면 파이썬 3를 해라. 위키책에 [[프로그래머가 아닌 이들을 위한 파이썬 3 자습서|이 책의 파이썬 3 버전]]이 있다. 일반인을 위한 파이썬 지침서는 파이썬 프로그래밍 언어에 대한 소개를 위하여 디자인된 지침서이다. 이 지침서는 프로그래밍 경험이 전혀 없는 사람을 위한 것이다. 여러분이 다른 언어로 프로그램 해 본 적이 있다면 귀도 반 로섬이 쓴 파이썬 지침서를 사용하기를 강력히 추천한다. 이 문서는 LaTeX, HTML, PDF, Postscript 등의 형태로 이용 가능하다. 여기로 가면 이러한 모든 형식을 볼 수 있다. 여러분이 질문이 있거나 덧붙이고자 하는 바가 있다면 jjc@iname.com로 나에게 연락을 취하라. 나는 이 문서에 관한 질문과 주석을 환영하는 바이다. 나는 될 수 있는한 최선을 다해서 어떠한 질문에 대해서도 응답하려고 노력할 것이다. 윈도우 설치 정보의 대부분을 작성해준 James A. Brown에게 감사드린다. 또한 본래의 지침서(일반인은 거의 사용할 수 없는 것이다.)에 관하여 불평을 해주고 또한 문서의 내용에 관하여 일일이 점검해준 Elizabeth Cogliati에게 감사드린다. 내가 빼먹었을 지도 모를 모든 분에게 감사드린다. 여기에 여러분이 유용하다고 생각할 만한 링크 몇 개를 나열한다. * [http://www.python.org/ 파이썬 홈페이지] * [http://www.python.org/doc/ 파이썬 문서] * [http://www.python.org/doc/current/tut/tut.html 프로그래머를 위한 파이썬 지침서] * [http://www.honors.montana.edu/~jjc/easytut/ LaTeX, PDF, and Postscript, 그리고 Zip 버전] 이 책을 Elizabeth Cogliati에게 바친다. bcng8dv7kz008ww2sfiprhcnatwqppa 48시간 안에 나만의 스킴 만들기/파싱 0 11990 45122 45121 2024-10-30T12:09:22Z Vpark45 8370 /* 리턴 값 */ 45122 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 안 맞는 타입이 있다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾼다. 5qkzquuj83hss9upbku5oqw4pwkbuar 45126 45122 2024-10-30T22:53:21Z Vpark45 8370 /* 리턴 값 */ 45126 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 안 맞는 타입이 있다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. tpg7rccme11gidbi2113y7j4esjqbap 45127 45126 2024-10-30T22:54:16Z Vpark45 8370 /* 리턴 값 */ 45127 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 4q1ufd0y2rn9c5n8a2nj9inyzmkyvb9 45128 45127 2024-10-30T23:03:44Z Vpark45 8370 /* 리턴 값 */ 45128 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> lghqzs0y0rsrtjbivh1eg7uchw63nxx 45129 45128 2024-10-30T23:10:21Z Vpark45 8370 /* 리턴 값 */ 45129 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 parseNumber를 다시 써보자. 이때 liftM은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 >>= 연산자 0xrexiz5nqffvjd90fmieyxon9b6n22 45130 45129 2024-10-30T23:10:48Z Vpark45 8370 /* 연습 문제 */ 45130 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 ss6o0gh4fc5k1gmxd3a91ur1peturxx 45131 45130 2024-10-31T02:25:30Z Vpark45 8370 /* 연습 문제 */ 45131 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능을 지원한다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. mmdehnig2kg8teiqm3ygoqfwzvtlqqp 45135 45131 2024-10-31T05:30:31Z Vpark45 8370 /* 연습 문제 */ 45135 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ** do표기법 ** 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능을 지원한다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. rwzuqcvjmmpr1widcuui8pl2ljesmcj 45136 45135 2024-10-31T05:30:41Z Vpark45 8370 /* 연습 문제 */ 45136 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. * do표기법 * 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능을 지원한다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. 3v0nk6ikafsaewqv3xbqv10m43r8685 45137 45136 2024-10-31T05:30:52Z Vpark45 8370 /* 연습 문제 */ 45137 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능을 지원한다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. mmdehnig2kg8teiqm3ygoqfwzvtlqqp 45138 45137 2024-10-31T05:31:31Z Vpark45 8370 /* 연습 문제 */ 45138 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. dvjysjmj9zdzm7a1ws0mh95or0e6put 45139 45138 2024-10-31T05:39:32Z Vpark45 8370 /* 연습 문제 */ 45139 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. guoz0t1uq31z1l5x45ihw4z3u30k38l 45140 45139 2024-10-31T05:43:17Z Vpark45 8370 /* 연습 문제 */ 45140 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. qsn3rqsnkhkhc17hk87icbhxqq1dwli 45141 45140 2024-10-31T05:48:06Z Vpark45 8370 /* 연습 문제 */ 45141 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. tni2ceaon3oiu3h20yvnb0fhfvwnu4p 45142 45141 2024-10-31T05:49:31Z Vpark45 8370 /* 연습 문제 */ 45142 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 4z7tc6z4rhr0tvg4l49c5vl18nekgwd 45143 45142 2024-10-31T05:56:53Z Vpark45 8370 /* 연습 문제 */ 45143 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) 7ue3wtuibloz78j6l420af3j66e7yw7 45144 45143 2024-10-31T06:57:12Z Vpark45 8370 /* 연습 문제 */ 45144 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> == 연습 문제 == # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> 7qmvhxozj6pybksfrfc1mayvhmsnw15 45145 45144 2024-10-31T06:57:46Z Vpark45 8370 /* 연습 문제 */ 45145 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> 8afd94q6q8yfqr4u2i8lb9slr2g7jy5 45146 45145 2024-10-31T07:59:30Z Vpark45 8370 /* 재귀 파서 */ 45146 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱한다. sw26goho1zafi2zn4ndx1f5zhjs4oeu 45147 45146 2024-10-31T08:03:38Z Vpark45 8370 /* 재귀 파서 */ 45147 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱해서 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 여기서 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. 3fhbfo8c4li1dn6z88mw4vekcf2exea 45148 45147 2024-10-31T08:04:01Z Vpark45 8370 /* 재귀 파서 */ 45148 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 여기서 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. b0b6lbo0wczn2rtrhqsf08lncg7nlxm 45149 45148 2024-10-31T08:04:23Z Vpark45 8370 /* 재귀 파서 */ 45149 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. lpnxylzrlhjao9za95czpe4fbuw398r 45150 45149 2024-10-31T08:05:51Z Vpark45 8370 /* 재귀 파서 */ 45150 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> tfn85bmp6q958y5ep3elyjkluqw3plg 45151 45150 2024-10-31T08:09:05Z Vpark45 8370 /* 재귀 파서 */ 45151 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 7srblib066oblme7301ltdivyz9qdw0 45152 45151 2024-10-31T08:09:53Z Vpark45 8370 /* 재귀 파서 */ 45152 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴한다. scz1fvs0xinro3uloy4in890drfo9sd 45153 45152 2024-10-31T08:13:26Z Vpark45 8370 /* 재귀 파서 */ 45153 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> rwjp5mdmw89zfrahfczwbire2wbhozx 45154 45153 2024-10-31T08:16:39Z Vpark45 8370 /* 재귀 파서 */ 45154 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴으로 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. m0vbdaayrvzgqyq2k96x6gx2ggvkn4u 45155 45154 2024-10-31T08:18:08Z Vpark45 8370 /* 재귀 파서 */ 45155 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. t6l5zfbn7nf2bsyi31z4648ow8wnguk 45156 45155 2024-10-31T08:22:18Z Vpark45 8370 /* 재귀 파서 */ 45156 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. <code>Atom</code> 생성자는 함수처럼 동작한다. <code>Atom</code>은 문자열을 감싸서 타입이 <code>LispVal</code>인 값을 준다. LispVal 타입의 값을 리스트에 넣는 것도 가능하다. 새로 만든 파서 parseQuoted를 parseExpr에서 사용할 수 있게 수정하자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber <|> parseQuoted <|> do char '(' x <- try parseList <|> parseDottedList char ')' return x </syntaxhighlight> hejie6tsycfmbq37w5r0n6wkm9jm7j6 45157 45156 2024-10-31T08:23:25Z Vpark45 8370 /* 재귀 파서 */ 45157 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. <code>Atom</code> 생성자는 함수처럼 동작한다. <code>Atom</code>은 문자열을 감싸서 타입이 <code>LispVal</code>인 값을 준다. LispVal 타입의 값을 리스트에 넣는 것도 가능하다. 새로 만든 파서 <code>parseQuoted</code>를 <code>parseExpr</code>에서 사용할 수 있게 수정하자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber <|> parseQuoted <|> do char '(' x <- try parseList <|> parseDottedList char ')' return x </syntaxhighlight> f7q37352dow4wtcrmj1u9e2r6wn2sje 45158 45157 2024-10-31T08:25:21Z Vpark45 8370 /* 재귀 파서 */ 45158 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. <code>Atom</code> 생성자는 함수처럼 동작한다. <code>Atom</code>은 문자열을 감싸서 타입이 <code>LispVal</code>인 값을 준다. LispVal 타입의 값을 리스트에 넣는 것도 가능하다. 새로 만든 파서 <code>parseQuoted</code>를 <code>parseExpr</code>에서 사용할 수 있게 수정하자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber <|> parseQuoted <|> do char '(' x <- try parseList <|> parseDottedList char ')' return x </syntaxhighlight> 위 코드는 Parsec의 마지막 기능인 백트래킹을 사용한 것이다. parseList와 parseDottedList는 t2xm1dfslj3iaakd3je7aeyb558g5ol 45159 45158 2024-10-31T08:30:37Z Vpark45 8370 /* 재귀 파서 */ 45159 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. <code>Atom</code> 생성자는 함수처럼 동작한다. <code>Atom</code>은 문자열을 감싸서 타입이 <code>LispVal</code>인 값을 준다. LispVal 타입의 값을 리스트에 넣는 것도 가능하다. 새로 만든 파서 <code>parseQuoted</code>를 <code>parseExpr</code>에서 사용할 수 있게 수정하자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber <|> parseQuoted <|> do char '(' x <- try parseList <|> parseDottedList char ')' return x </syntaxhighlight> 위 코드는 Parsec의 마지막 기능인 백트래킹을 사용한 것이다. parseList와 parseDottedList는 점이 나올 때까지 같은 문자열을 파싱해야 한다. 이때 parseList가 파싱을 실패하면 parseDottedList도 같은 문자열을 처음부터 다시 읽어야 한다. try 콤비네이터는 먼저 특정 파서를 실행하고 만약 파싱이 실패하면 이전 상태로 돌아간다. try 콤비네이터는 다른 파서를 방해하지 않고 <|>와 같이 선택 하는 파서를 쓸 수 있게 해준다. 코드를 컴파일하고 실행하면 다음과 같이 나온다. <pre> $ cabal run myProject "(a test)" Found value $ cabal run myProject "(a (nested) test)" Found value $ cabal run myProject "(a (dotted . list) test)" Found value $ cabal run myProject "(a '(quoted (dotted . list)) test)" Found value $ cabal run myProject "(a '(imbalanced parens)" No match: "lisp" (line 1, column 24): unexpected end of input expecting space or ")" </pre> ebvmlotmmxhe5x9tmfrd3lceofs7366 45160 45159 2024-10-31T11:20:04Z Vpark45 8370 /* 재귀 파서 */ 45160 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. <code>Atom</code> 생성자는 함수처럼 동작한다. <code>Atom</code>은 문자열을 감싸서 타입이 <code>LispVal</code>인 값을 준다. LispVal 타입의 값을 리스트에 넣는 것도 가능하다. 새로 만든 파서 <code>parseQuoted</code>를 <code>parseExpr</code>에서 사용할 수 있게 수정하자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber <|> parseQuoted <|> do char '(' x <- try parseList <|> parseDottedList char ')' return x </syntaxhighlight> 위 코드는 Parsec의 마지막 기능인 백트래킹을 사용한 것이다. parseList와 parseDottedList는 점이 나올 때까지 같은 문자열을 파싱해야 한다. 이때 parseList가 파싱을 실패하면 parseDottedList도 같은 문자열을 처음부터 다시 읽어야 한다. try 콤비네이터는 먼저 특정 파서를 실행하고 만약 파싱이 실패하면 이전 상태로 돌아간다. try 콤비네이터는 다른 파서를 방해하지 않고 <|>와 같이 선택 하는 파서를 쓸 수 있게 해준다. 코드를 컴파일하고 실행하면 다음과 같이 나온다. <pre> $ cabal run myProject "(a test)" Found value $ cabal run myProject "(a (nested) test)" Found value $ cabal run myProject "(a (dotted . list) test)" Found value $ cabal run myProject "(a '(quoted (dotted . list)) test)" Found value $ cabal run myProject "(a '(imbalanced parens)" No match: "lisp" (line 1, column 24): unexpected end of input expecting space or ")" </pre> 파서 안에서 <code>parseExpr</code>를 참조함으로써 파서를 임의로 깊게 중첩할 수 있다. 이로 인해 몇 가지 정의만으로 완전한 리스프 파서를 구축할 수 있다. 이것이 바로 재귀의 힘이다. kir631bzo0gryhja3pml3zoaop27obq 45161 45160 2024-10-31T11:29:33Z Vpark45 8370 /* 재귀 파서 */ 45161 wikitext text/x-wiki [[분류:책:48시간 안에 나만의 스킴 만들기]] == 프로젝트 뼈대 생성과 Parsec 설치하기 == 이 절에서는 [https://github.com/aslatter/parsec Parsec] 라이브러리를 사용한다. Parsec 라이브러리를 설치하려면 <code>cabal</code> 명령어가 필요하다. 리눅스에서는 [https://www.haskell.org/ghcup/ GHCup]을 이용해서 <code>cabal</code>을 설치할 수 있다. 다음과 같이 프로젝트를 만들자. <pre> $ cabal update $ mkdir myProject $ cd myProject $ cabal init --simple --minimal --exe </pre> 이제 <code>myProject.cabal</code>을 수정하자. 다음과 같이 <code>build-depends</code>란에 <code>base</code> 외에 <code>parsec</code>을 추가한다. <pre> build-depends: base, parsec </pre> <code>cabal run</code>을 입력해서 프로젝트를 실행하자. <pre> Building executable 'myProject' for myProject-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs ) Linking myProject-0.1.0.0/x/myProject/build/myProject/myProject ... Hello, Haskell! </pre> 마지막 줄은 프로그램이 출력한 것이다. == 간단한 파서 만들기 == 엄청 간단한 파서를 하나 만들어 보자. <code>app/Main.hs</code> 파일(<code>cabal init</code>이 자동으로 만든 파일이다.)에 아래와 같은 코드를 추가해보자. <syntaxhighlight lang="haskell"> import Text.ParserCombinators.Parsec hiding (spaces) import System.Environment </syntaxhighlight> 위와 같이 적으면 <code>spaces</code> 함수만 빼고 Parsec 라이브러리를 사용할 수 있다. <code>spaces</code>라는 함수 이름은 나중에 따로 만들 거라 이름이 겹쳐서 뺐다. 다음과 같이 파서를 하나 정의한다. 이 파서는 스킴 식별자 중 하나인 심볼을 인식한다. <syntaxhighlight lang="haskell"> symbol :: Parser Char symbol = oneOf "!#$%&|*+-/:<=>?@^_~" </syntaxhighlight> <code>Parser Char</code>도 모나드이다. 여기서 숨겨진 “추가 정보”는 입력 스트림에서 위치 정보, 백트래킹 기록 등에 관한 모든 정보이다. Parsec이 이 모든 것을 처리해준다. Parsec 라이브러리에서 함수 <code>oneOf</code>만 사용하면 된다. <code>oneOf</code> 함수는 인자로 넣은 문자열 중에 한 글자를 식별한다. Parsec에 <code>letter</code>와 <code>digit</code> 같은 내장 파서가 있다. 기본 파서를 조합해서 더 정교한 파서를 만들 수 있다. 파서를 호출하고 에러를 처리하는 함수를 정의해보자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse symbol "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 타입 서명을 보면 <code>readExpr</code>은 <code>String</code>을 넣으면 <code>String</code>이 나오는 함수(<code><nowiki>-></nowiki></code>)이다. 매개변수 이름을 <code>input</code>으로 짓고 위에서 정의한 <code>symbol</code> 파서와 함께 Parsec 함수 <code>parse</code>에 넘긴다. <code>parse</code>의 두 번째 매개변수는 입력에 대한 이름인데 에러 메세지에서 쓰인다. <code>parse</code>는 파싱된 값이나 에러를 리턴한다. 따라서 <code>parse</code>를 쓰다 에러가 나도 처리가 가능하다. 일반적인 하스켈 관례에 따라 Parsec은 <code>Either</code> 데이터 타입을 리턴한다. 이때 <code>Left</code> 생성자는 에러를 나타내고 <code>Right</code> 생성자는 정상 값을 나타낸다. <code>case...of</code> 구문을 사용해서 <code>parse</code>의 여러 결과를 매칭한다. 결과가 <code>Left</code>일 때 에러를 변수 <code>err</code>에 연결하고 문자열 <code>"No match"</code>와 문자열로 변환한 에러를 리턴한다. 결과가 <code>Right</code> 값이면 결과를 변수 <code>val</code>에 연결하고 문자열 <code>"Found value"</code>를 리턴한다. <code>case...of</code> 구문은 패턴 매칭의 예시이다. 패턴 매칭은 나중에 더 자세히 다룬다. 마지막으로 <code>main</code> 함수가 <code>readExpr</code>을 호출하도록 변경하고 결과를 출력하자. <syntaxhighlight lang="haskell"> main :: IO () main = do (expr:_) <- getArgs putStrLn (readExpr expr) </syntaxhighlight> 위 코드를 컴파일하고 실행하려면 다음과 같이 명령줄에서 프로젝트 이름을 적고 매개변수를 넣어야 한다. <pre> $ cabal run myProject $ Found value $ cabal run myProject a No match: "lisp" (line 1, column 1): unexpected "a" </pre> == 공백 == 이제 점차 더 복잡한 표현식을 인식할 수 있도록 파서를 단계적으로 개선해보자. 현재 파서는 심볼 앞에 공백이 있으면 에러가 난다. <pre> $ cabal run myProject " %" No match: "lisp" (line 1, column 1): unexpected " " </pre> 코드를 고쳐서 공백을 무시해보자. 여러 공백 문자를 인식하는 파서를 정의하자. Parsec 라이브러리에 이미 <code>spaces</code> 함수가 있기 때문에 Parsec을 임포트할 때 <code>hiding (spaces)</code> 구문을 썼다. 별로 여기서 필요한 함수는 아니다.(여기서는 <code>lexeme</code>이라는 파서를 써도 되지만 교육 목적을 위해 직접 구현한다.) <syntaxhighlight lang="haskell"> spaces :: Parser () spaces = skipMany1 space </syntaxhighlight> 함수를 함수에 넣을 수 있듯이 액션도 액션에 넣을 수 있다. 여기서는 Parser 액션 <code>space</code>를 Parser 액션 <code>skipMany1</code>에 넣었다. 이렇게 만든 파서는 하나 이상의 공백을 인식할 수 있다. <code>parse</code> 함수를 고쳐서 새로 만든 파서 <code>spaces</code>를 써보자. <syntaxhighlight lang="haskell"> readExpr input = case parse (spaces >> symbol) "lisp" input of Left err -> "No match: " ++ show err Right val -> "Found value" </syntaxhighlight> 2장에서 <code>>></code>(“바인드”) 연산자를 잠깐 언급했다. do블록 안에서 여러 행을 합칠 때(눈에 보이진 않지만) 바인드 연산자가 쓰인다. 여기서는 <code>spaces</code> 파서와 <code>symbol</code> 파서를 합치려고 명시적으로 바인드 연산자를 사용했다. 그런데 바인드는 Parser와 IO 모나드에서 완전히 다르게 동작한다. Parser 모나드에서 바인드는 다음과 같이 동작한다. “첫 번째 파서로 매칭을 시도하고 나머지 입력은 두 번째 파서로 매칭을 시도한다. 둘 다 실패하면 결과는 실패이다”. 일반적으로 바인드는 서로 다른 모나드에서 완전히 다른 영향을 미친다. 모나드는 계산을 구조화하는 일반적인 방법으로써 설계되었다. 모나드는 다양한 계산을 수용할 수 있을만큼 일반적이어야 한다. 모나드가 정확히 무엇을 하는지 알고 싶다면 모나드 관련 문서를 읽어보자. 코드를 컴파일하고 실행하자. <code>spaces</code>를 <code>skipMany1</code>으로 정의했기 때문에 이제 더 이상 프로그램이 한 글자는 인식을 못한다. 대신 심볼 앞에 먼저 공백 문자를 넣어야 한다. 다음과 같이 이 파서가 어떻게 유용한지 확인해보자. <pre> $ cabal run myProject " %" Found value $ cabal run myProject % No match: "lisp" (line 1, column 1): unexpected "%" expecting space $ cabal run myProject " abc" No match: "lisp" (line 1, column 4): unexpected "a" expecting space </pre> == 리턴 값 == 아직 파서가 많은 일을 하지는 못한다. 현재 버전의 파서는 문자열을 입력하면 인식할 수 있는지, 없는지 알려줄 뿐이다. 파서는 더 많은 일을 할 수 있어야 한다. 파서가 입력 받은 문자열을 쉽게 탐색할 수 있는 자료 구조로 변환할 수 있으면 좋겠다. 이 절에서는 데이터 타입을 정의하고 파서가 데이터 타입을 리턴하는 방법을 배운다. 리스프 값을 담을 수 있는 데이터 타입을 정의하자. <syntaxhighlight lang="haskell"> data LispVal = Atom String | List [LispVal] | DottedList [LispVal] LispVal | Number Integer | String String | Bool Bool </syntaxhighlight> 위 코드는 대수적 데이터 타입의 예시이다. 이 타입은 LispVal 타입의 변수가 담을 수 있는 여러 값의 집합을 정의한다. <code>|</code>로 구분된 여러 값은 생성자 태그와 데이터를 담고 있다. 위 코드에서 LispVal은 다음과 같은 값 중 하나가 될 수 있다. ;<code>Atom</code>:아톰이라고 불리는 문자열을 저장한다. ;<code>List</code>:다른 여러 <code>LispVal</code> 타입의 값을 저장하는 리스트이다.(하스켈 리스트는 대괄호로 표기한다.) 올바른 리스트라고 하기도 한다. ;<code>DottedList</code>:<code>(a b . c)</code>와 같은 스킴 폼을 나타낸다. 올바르지 않은 리스트라고 하기도 한다. ;<code>Number</code>:하스켈 정수 타입을 담는다. ;<code>String</code>:하스켈 문자열 타입을 담는다. ;<code>Bool</code>:하스켈 불린(boolean) 타입 값을 담는다. 생성자와 타입은 서로 다른 이름공간에 존재한다. 따라서 <code>String</code>이라는 이름을 생성자에도, 타입 이름에도 같이 쓸 수 있다. 타입과 생성자 이름은 항상 대문자로 시작해야 한다. 위에서 만든 타입을 만드는 파싱 함수를 추가해보자. 문자열은 큰따옴표로 시작하고 큰따옴표가 아닌 글자가 몇 글자 나오고 큰따옴표로 끝나는 것이다. <syntaxhighlight lang="haskell"> parseString :: Parser LispVal parseString = do char '"' x <- many (noneOf "\"") char '"' return $ String x </syntaxhighlight> <code>>></code> 연산자를 쓰는 대신 다시 do표기법을 썼다. 왜냐하면 중간에 다른 파싱 동작을 하고 <code>many (noneOf "\"")</code>가 리턴한 값을 받아두었다가 나중에 써야 하기 때문이다. 액션이 값을 리턴하지 않으면 <code>>></code>를 쓰자. 액션이 값을 리턴하고 그 값을 다음 액션으로 넘겨야 할 때는 <code>>>=</code>를 쓰자. 나머지 경우에는 do표기법을 쓰면 된다. 파싱이 끝나면 `many`가 리턴한 하스켈 문자열에 <code>String</code> 생성자를 적용해서 하스켈 문자열을 <code>LispVal</code>로 만든다. 대수적 데이터 타입의 모든 생성자는 함수처럼 동작한다. 생성자도 함수처럼 인자로 받은 것을 값으로 바꾼다. 생성자는 패턴 매칭 표현식의 왼쪽에서 패턴으로 쓸 수도 있다. <code>Either</code> 데이터 타입의 두 생성자를 패턴 매칭하는 예제를 [[#간단한 파서 만들기]]에서 소개했다. <code>LispVal</code> 타입의 값을 <code>Parser</code> 모나드로 바꾸려면 내장 함수 <code>return</code>을 쓴다. do블록의 각 행은 모두 타입이 <code>Parser LispVal</code>로 같아야 한다. 그런데 <code>String</code> 생성자의 결과는 그냥 <code>LispVal</code>이다. <code>return</code>은 <code>LispVal</code>을 감싸서 아무 일도 안 하고 값만 돌려주는 Parser 액션에 넣어준다. 전체 <code>parseString</code> 액션의 타입은 결국 <code>Parser LispVal</code>이 된다. <code>$</code> 연산자는 중위 함수이다. 아래 두 코드는 같다. <syntaxhighlight lang="haskell"> return $ String x return (String x) </syntaxhighlight> <code>$</code>는 결합 방향이 오른쪽이고 우선순위가 낮아서 괄호를 적지 않을 수 있게 해준다. <code>$</code>는 연산자라서 함수에 할 수 있는 건 똑같이 다 할 수 있다.(연산자를 인자로 넘기거나 부분만 적용할 수 있다.) <code>$</code>는 리스프 함수 <code>apply</code>와 비슷하다. 이제 스킴 변수를 다뤄보자. 아톰은 글자나 심볼로 시작하고 글자, 숫자, 심볼 등을 여러 개 쓸 수 있다. <syntaxhighlight lang="haskell"> parseAtom :: Parser LispVal parseAtom = do first <- letter <|> symbol rest <- many (letter <|> digit <|> symbol) let atom = first:rest return $ case atom of "#t" -> Bool True "#f" -> Bool False _ -> Atom atom </syntaxhighlight> <code><|></code>는 선택 연산자이다. <code><|></code>는 첫 번째 파서를 시도해보고 실패하면 두 번째 파서를 시도한다. 둘 중 하나가 성공하면 성공한 파서가 리턴한 값을 리턴한다. 첫 번째 파서가 실패할 때는 파싱할 글자를 소모하지 않는다. 백트래킹 구현하는 방법은 나중에 소개한다. 아톰의 첫 번째 글자와 나머지를 읽고나서 첫 번째 글자와 나머지를 합쳐야 한다. <code>let</code> 구문은 새 변수 <code>atom</code>을 정의한다. 첫 번째 글자와 나머지를 합칠 때 리스트 생성 연산자 <code>:</code>를 사용한다. <code>:</code> 대신 리스트 연결 연산자 <code>++</code>를 써도 된다.(<code>[first] ++ rest</code>) <code>first</code>는 리스트가 아니라 그냥 문자라서 대괄호로 감싸 리스트로 만들어야 <code>++</code>로 <code>rest</code>와 합칠 수 있다. case 표현식으로 <code>LispVal</code> 타입 중 어떤 값을 만들고 리턴할지 정한다. 매칭 결과가 리스프 참, 거짓 리터럴인 경우 하스켈 참, 거짓을 리턴한다. 밑줄 <code>_</code>은 가독성을 위해 사용한다. case 블록은 위에서부터 참이 없는 경우 <code>_</code>를 만날 때까지 계속 내려간다.(<code>_</code>가 없고 모든 케이스가 실패할 경우 전체 case 표현식 자체가 실패한다.) <code>_</code>는 와일드카드이다. 모든 케이스가 실패하고 <code>_</code>까지 오면 매칭이 되고 <code>atom</code>을 리턴한다. 마지막으로 숫자 파서만 하나 더 만들자. 모나딕 값을 어떻게 다루는지 보자. <syntaxhighlight lang="haskell"> parseNumber :: Parser LispVal parseNumber = liftM (Number . read) $ many1 digit </syntaxhighlight> 위 코드는 오른쪽부터 거꾸로 읽는 게 편하다. 왜냐하면 함수 적용(<code>$</code>)과 함수 합성(<code>.</code>) 모두 결합 방향이 오른쪽이기 때문이다. 파섹 콤비네이터 <code>many1</code>은 인자로 넣은 파서가 파싱 할 수 있는 글자를 한 개 이상 파싱한다. 문자열을 파싱해서 <code>LispVal</code> 타입의 숫자 값을 리턴하고 싶은데 타입이 맞질 않는다. 먼저 내장 함수 <code>read</code>를 써서 문자열을 숫자로 바꾸고 <code>Number</code> 생성자에 넣어서 <code>LispVal</code> 타입으로 만든다. 표준 함수 <code>liftM</code>으로도 같은 일을 할 수 있다. <code>liftM</code>을 <code>Number . read</code>에 적용하고 파싱한 문자열에 적용하면 된다. <code>liftM</code>을 쓰려면 다음과 같이 프로그램 제일 위에 <code>Monad</code> 모듈을 임포트해야 한다. <syntaxhighlight lang="haskell"> import Control.Monad </syntaxhighlight> 함수 합성, 함수 적용, 함수에 함수를 인자로 넣는 프로그래밍 방식은 하스켈 코드에서 흔하다. 이런 프로그래밍 방식은 복잡한 알고리즘을 한 줄로 표현할 수 있게 해주고 중간 과정을 여러 함수로 나눠서 조합하기 좋게 만든다. 하스켈 코드를 오른쪽부터 왼쪽으로 읽어야 한다거나 타입을 잘 기억해야 한다는 점은 불편할 수 있다. 이 책에서 많은 예제를 접하면서 익숙해지길 바란다. 문자열, 숫자, 아톰을 파싱하는 파서를 만들어 보자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber </syntaxhighlight> readExpr가 parseExpr을 쓰도록 수정하자. <syntaxhighlight lang="haskell"> readExpr :: String -> String readExpr input = case parse parseExpr "lisp" input of Left err -> "No match: " ++ show err Right _ -> "Found value" </syntaxhighlight> 위 코드를 컴파일하고 실행하면 다음과 같이 프로그램이 숫자, 문자열, 심볼은 파싱하지만 다른 것은 파싱하지 못하는 것을 볼 수 있다. <pre> $ cabal run myProject "\"this is a string\"" Found value $ cabal run myProject 25 Found value $ cabal run myProject symbol Found value $ cabal run myProject (symbol) bash: syntax error near unexpected token `symbol' $ cabal run myProject "(symbol)" No match: "lisp" (line 1, column 1): unexpected "(" expecting letter, "\"" or digit </pre> === 연습 문제 === # 다음 항목을 써서 <code>parseNumber</code>를 다시 써보자. 이때 <code>liftM</code>은 쓰면 안 된다. ## do표기법 ## 일련의 명시적인 <code>>>=</code> 연산자 # R<sup>5</sup>RS는 문자열 안에서 인용 부호를 이스케이프 처리하는 기능이 있다. <code>parseString</code>을 고쳐서 <code>\"</code>를 만나도 문자열이 끝나지 않게 해보자. <code>noneOf "\""</code> 대신 새로운 파서 액션을 만들어 보자. 이 액션은 인용 부호가 아닌 글자를 파싱하거나 백슬래시 다음에 인용 부호가 오는 문자열을 파싱할 수 있다. # <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\\</code> 등 이스케이프 문자를 지원하도록 앞 연습 문제를 수정해보자. # 다른 진법을 지원하는 스킴 표준을 따라서 <code>parseNumber</code>를 바꿔보자. <code>readOct</code>와 <code>readHex</code> 함수를 써보자. # <code>LispVal</code> 타입에 <code>Character</code> 생성자를 추가하자. R<sup>5</sup>RS에 나온 문자 리터럴을 파싱하는 파서를 만들자. # <code>LispVal</code> 타입에 <code>Float</code> 생성자를 추가하자. R<sup>5</sup>RS 소수점 문법을 지원하자. 하스켈 함수 <code>readFloat</code>을 써보자. # 다양한 스킴 숫자 유형을 모두 지원하기 위해 데이터 타입과 파서를 추가하자. 하스켈에 숫자를 표현하는 다양한 내장 타입이 있다. 하스켈 표준 라이브러리 <code>Prelude</code>를 참고하자. <code>Rational</code> 타입은 분모와 분자료 표현할 수 있고 <code>Complex</code> 타입은 실수부와 허수부로 표현할 수 있다.(실수부와 허수부 각각을 <code>Real</code> 타입으로 표현할 수 있다.) == 재귀 파서 == 인터프리터에 파서 액션 몇 개를 더해보자. 그 유명한 리스프의 괄호 파싱부터 해보자. <syntaxhighlight lang="haskell"> parseList :: Parser LispVal parseList = liftM List $ sepBy parseExpr spaces </syntaxhighlight> <code>parseList</code>는 <code>parseNumber</code>와 비슷하게 동작한다. 먼저 <code>sepBy parseExpr spaces</code>는 공백으로 나뉜 표현식을 파싱하고 <code>Parser</code> 모나드 안에서 <code>List</code> 생성자에 적용한다. 직접 만든 <code>parseExpr</code> 액션도 <code>sepBy</code>에 넣을 수 있다는 점을 잘 보자. dotted 리스트 파서는 복잡해보이지만 개념 자체는 복잡하지 않다. <syntaxhighlight lang="haskell"> parseDottedList :: Parser LispVal parseDottedList = do head <- endBy parseExpr spaces tail <- char '.' >> spaces >> parseExpr return $ DottedList head tail </syntaxhighlight> 일련의 <code>Parser</code> 액션을 <code>>></code>로 어떻게 연결하고, 어떻게 <code><-</code> 오른쪽에 적었는지 잘 보자. 표현식 <code>char '.' >> spaces</code>는 <code>Parser ()</code>을 리턴하고 <code>parseExpr</code>와 연결하면 <code>Parser LispVal</code>이 된다. do블록의 모든 줄은 각각 타입이 <code>Parser LispVal</code>이어야 한다. 스킴 문법 설탕인 작은따옴표 파싱 기능을 추가하자. <syntaxhighlight lang="haskell"> parseQuoted :: Parser LispVal parseQuoted = do char '\'' x <- parseExpr return $ List [Atom "quote", x] </syntaxhighlight> 위 코드는 작은따옴표 하나를 읽은 다음 표현식을 읽어서 변수 <code>x</code>에 연결하고 스킴 문법으로 표현하면 <code>(quote x)</code>를 리턴한다. <code>Atom</code> 생성자는 함수처럼 동작한다. <code>Atom</code>은 문자열을 감싸서 타입이 <code>LispVal</code>인 값을 준다. LispVal 타입의 값을 리스트에 넣는 것도 가능하다. 새로 만든 파서 <code>parseQuoted</code>를 <code>parseExpr</code>에서 사용할 수 있게 수정하자. <syntaxhighlight lang="haskell"> parseExpr :: Parser LispVal parseExpr = parseAtom <|> parseString <|> parseNumber <|> parseQuoted <|> do char '(' x <- try parseList <|> parseDottedList char ')' return x </syntaxhighlight> 위 코드는 Parsec의 마지막 기능인 백트래킹을 사용한 것이다. parseList와 parseDottedList는 점이 나올 때까지 같은 문자열을 파싱해야 한다. 이때 parseList가 파싱을 실패하면 parseDottedList도 같은 문자열을 처음부터 다시 읽어야 한다. try 콤비네이터는 먼저 특정 파서를 실행하고 만약 파싱이 실패하면 이전 상태로 돌아간다. try 콤비네이터는 다른 파서를 방해하지 않고 <|>와 같이 선택 하는 파서를 쓸 수 있게 해준다. 코드를 컴파일하고 실행하면 다음과 같이 나온다. <pre> $ cabal run myProject "(a test)" Found value $ cabal run myProject "(a (nested) test)" Found value $ cabal run myProject "(a (dotted . list) test)" Found value $ cabal run myProject "(a '(quoted (dotted . list)) test)" Found value $ cabal run myProject "(a '(imbalanced parens)" No match: "lisp" (line 1, column 24): unexpected end of input expecting space or ")" </pre> 파서 안에서 <code>parseExpr</code>를 참조함으로써 파서를 임의로 깊게 중첩할 수 있다. 이로 인해 몇 가지 정의만으로 완전한 리스프 파서를 구축할 수 있다. 이것이 바로 재귀의 힘이다. === 연습 문제 === # 억음 부호 문법 설탕 기능을 추가하자. 스킴 표준에 따라 억음 부호를 쓰면 쿼시쿼트와 언쿼트를 할 수 있어야 한다. # 벡트 기능을 추가하자. 하스켈 표현 방식은 자유롭게 하면 된다. GHC에 <code>Array</code> 데이터 타입이 있는데 쓰기 쉽진 않다. 벡터는 상수 시간 색인과 수정이 되어야 한다. 그런데 순수 함수형 언어에서 파괴적 수정은 어렵다. 이 튜토리얼 뒤에서 <code>set!</code>을 다루는데 <code>set!</code>에서 힌트를 얻을 수 있다. s4wxdikyzn14k4tksdnfojdmmgjhchf 일반인을 위한 파이썬 지침서/문자열의 복수 0 11991 45163 2024-10-31T11:40:41Z Vpark45 8370 새 문서: 이제 문자열로 할 수 있는 기가 막힌 꼼수하나를 보이겠다. <pre> def shout(string): for character in string: print "Gimme a "+character print "'"+character+"'" shout("Lose") def middle(string): print "The middle character is:",string[len(string)/2] middle("abcdefg") middle("The Python Programming Language") middle("Atlanta") </pre> 그 출력은 다음과 같다. <pre> Gimme a L 'L' Gimme a o 'o' Gimme a s 's' Gimme a e 'e' The midd... 45163 wikitext text/x-wiki 이제 문자열로 할 수 있는 기가 막힌 꼼수하나를 보이겠다. <pre> def shout(string): for character in string: print "Gimme a "+character print "'"+character+"'" shout("Lose") def middle(string): print "The middle character is:",string[len(string)/2] middle("abcdefg") middle("The Python Programming Language") middle("Atlanta") </pre> 그 출력은 다음과 같다. <pre> Gimme a L 'L' Gimme a o 'o' Gimme a s 's' Gimme a e 'e' The middle character is: d The middle character is: r The middle character is: a </pre> 이 프로그램이 보여주는 것은 문자열이 여러 면으로 리스트와 비슷하다는 것이다. shout 프로시져는 for회돌이가 리스트에 사용될 수 있는 것과 마찬가지로 문자열에 사용될 수 있다는 것을 보여준다. middle 프로시져는 그 문자열이 len함수 그리고 배열 지표 그리고 썰기 역시 사용할 수 있음을 보여준다. 대부분의 리스트 사양은 문자열에도 또한 작동한다. 다음의 사양은 문자열의 어떤 특별한 사양을 보여준다. <pre> def to_upper(string): ## Converts a string to upper case upper_case = "" for character in string: if 'a' <= character <= 'z': location = ord(character) - ord('a') new_ascii = location + ord('A') character = chr(new_ascii) upper_case = upper_case + character return upper_case print to_upper("This is Text") </pre> 그 출력을 여기에 보이면 <pre> THIS IS TEXT </pre> 컴퓨터가 문자열의 문자를 0에서 255까지의 숫자로 나타내므로 이것은 잘 작동한다. 파이썬은 ord( ordinal의 약자)라 부르는 함수를 가지고 있어서 문자를 숫자로 반환해 준다. 또한 대응되는 chr 이라 부르는 함수도 있어서 숫자를 문자로 반환해준다. 이것을 염두에 두고서 프로그램은 깨끗해지기 시작해야만 한다. 첫 번째 세부사항은 이 라인이다: 'if 'a' <= character <= 'z':' 이 라인은 문자가 소문자인가 점검한다. 만약 그렇다면 다음의 라인이 사용된다. 먼저 그것은 'location = ord(character) - ord('a')' 라인을 사용하여 위치로 변환되어 a=0,b=1,c=2 등등으로 변환된다. 다음으로 그 새로운 값은 'new_ascii = location + ord('A')'에서 발견된다. 이 값은 다시 문자로 변환되어서 이제는 대문자이다. 이제 더 짧은 타이핑 연습을 위해 <pre> print "Integer to String" print repr(2) print repr(23445) print repr(-23445) print "String to Integer" print int("14234") print int("12345") print int("-3512") print "Float to String" print repr(234.423) print repr(62.562) print repr(-134.5660) print "Float to Integer" print int(51.523) print int(224.63) print int(-1234.562) </pre> 친숙하게 보이는 출력은 다음과 같다. <pre> Integer to String 2 23445 -23445 String to Integer 14234 12345 -3512 Float to String 234.423 62.562 -134.566 Float to Integer 51 224 -1234 </pre> 여러분이 아직까지 이해가 가지 않는다면 (역주: 가르쳐 주면) repr 함수가 정수를 문자열로, int함수가 문자열을 정수로 변환할수 있다. repr 함수는 어떤 것의 출력가능한 형태를 돌려준다. 여기에 이러한 약간의 예가 있다. <pre> >>> repr(1) '1' >>> repr(234.14) '234.14' >>> repr([4,42,10]) '[4, 42, 10]' </pre> int 함수는 문자열(혹은 소수형)을 정수로 변환한다. float 라고 불리는 비슷한 함수 또한 있어서 정수나 문자열을 소수형으로 바꾸어 준다. 파이썬이 가지고 있는 또 다른 함수는 eval 함수이다. eval 함수는 문자열을 취해 파이썬이 판단하는 형태로 데이타를 반환한다. 예를 들어 <pre> >>> v=eval('123') >>> print v,type(v) 123 <type 'int'> <- 정수형 >>> v=eval('645.123') >>> print v,type(v) 645.123 <type 'float'> <- 소수형 >>> v=eval('[1,2,3]') >>> print v,type(v) [1, 2, 3] <type 'list'> <- 리스트형 </pre> 여러분이 eval 함수를 사용한다면 그 함수가 여러분이 예상한 형태를 반환하는지 점검해야 한다. 하나의 유용한 문자열 함수는 split 함수이다. 여기에 예가 있다. <pre> >>> import string >>> string.split("This is a bunch of words") ['This', 'is', 'a', 'bunch', 'of', 'words'] >>> string.split("First batch, second batch, third, fourth",",") ['First batch', ' second batch', ' third', ' fourth'] </pre> split이 문자열을 문자열의 리스트로 변환하는 것을 주목하라. 문자열은 기본값으로 공백으로 분리되거나 혹은 선택적으로 두 번째 인수에 의해서 분리된다 (이경우에는 콤마). == 예제 == <pre> # This program requires a excellent understanding of decimal numbers def to_string(in_int): "Converts an integer to a string" out_str = "" prefix = "" if in_int < 0: prefix = "-" in_int = -in_int while in_int / 10 != 0: out_str = chr(ord('0')+in_int % 10) + out_str in_int = in_int / 10 out_str = chr(ord('0')+in_int % 10) + out_str return prefix + out_str def to_int(in_str): "Converts a string to an integer" out_num = 0 if in_str[0] == "-": multiplier = -1 in_str = in_str[1:] else: multiplier = 1 for x in range(0,len(in_str)): out_num = out_num * 10 + ord(in_str[x]) - ord('0') return out_num * multiplier print to_string(2) print to_string(23445) print to_string(-23445) print to_int("14234") print to_int("12345") print to_int("-3512") </pre> 그 출력은 다음과 같다. <pre> 2 23445 -23445 14234 12345 -3512 </pre> 9ybwp8r4qhm6bo1x4lfc3s3oa4cdr5w 일반인을 위한 파이썬 지침서/파일 입출력 0 11992 45164 2024-10-31T11:43:21Z Vpark45 8370 새 문서: 여기에 간단한 파일 입출력의 예가 하나 있다. <pre> # Write a file out_file = open("test.txt","w") out_file.write("This Text is going to out file\nLook at it and see\n") out_file.close() #Read a file in_file = open("test.txt","r") text = in_file.read() in_file.close() print text, </pre> test.txt 파일의 내용과 출력은 다음과 같다. <pre> This Text is going to out file Look at it and see </pre> 여러분이 프로그램을 실행한 디렉토리에... 45164 wikitext text/x-wiki 여기에 간단한 파일 입출력의 예가 하나 있다. <pre> # Write a file out_file = open("test.txt","w") out_file.write("This Text is going to out file\nLook at it and see\n") out_file.close() #Read a file in_file = open("test.txt","r") text = in_file.read() in_file.close() print text, </pre> test.txt 파일의 내용과 출력은 다음과 같다. <pre> This Text is going to out file Look at it and see </pre> 여러분이 프로그램을 실행한 디렉토리에 그 프로그램이 test.txt 라는 불리우는 파일을 작성하고 있음을 주목하라. 문자열 속의 '\n'은 파이썬에게 바로 그 지점에서 새로운 라인을 출력하라고 지시한다. 파일 입출력의 개요는 다음과 같다. * open 함수로 파일 객체를 가져온다. * 그 파일 객체에 대하여 (어떤 상태로 열렸느냐에 따라) 읽기 또는 쓰기를 한다. * 그것을 닫는다. 첫째 단계는 파일객체를 획득하는 것이다. 이것을 하는 방법은 open 함수를 사용하는 것이다. 그 형식은 file_object = open(filename,mode)이며 file_object 는 파일 객체를 담을 변수이다, filename 화일이름을 나타내는 문자열이며, mode 는 "r"로 읽기(read)를, "w" 로 쓰기(w) 를 나타낸다. 다음으로 그 파일 객체의 함수는 호출될 수 있다. 두개의 가장 일반적인 함수는 read 와 write 이다. write 함수는 문자열을 파일의 끝에다가 추가한다. read 함수는 파일에서 다음의 것을 읽는다 그리고 그것을 문자열로 반환한다. 만일 아무런 인수도 주어지지 않는다면 그것은 (이 예제에서 실행된 것 같이) 전체 화일을 반환할 것이다. 이제 여기에 우리가 이전에 만들었던 전화 번호 프로그램의 새로운 버전이 있다. <pre> import string def print_numbers(numbers): print "Telephone Numbers:" for x in numbers.keys(): print "Name: ",x," \tNumber: ",numbers[x] print def add_number(numbers,name,number): numbers[name] = number def lookup_number(numbers,name): if numbers.has_key(name): return "The number is "+numbers[name] else: return name+" was not found" def remove_number(numbers,name): if numbers.has_key(name): del numbers[name] else: print name," was not found" def load_numbers(numbers,filename): in_file = open(filename,"r") while 1: in_line = in_file.readline() if in_line == "": break in_line = in_line[:-1] [name,number] = string.split(in_line,",") numbers[name] = number in_file.close() def save_numbers(numbers,filename): out_file = open(filename,"w") for x in numbers.keys(): out_file.write(x+","+numbers[x]+"\n") out_file.close() def print_menu(): print '1. Print Phone Numbers' print '2. Add a Phone Number' print '3. Remove a Phone Number' print '4. Lookup a Phone Number' print '5. Load numbers' print '6. Save numbers' print '7. Quit' print phone_list = {} menu_choice = 0 print_menu() while menu_choice != 7: menu_choice = input("Type in a number (1-7):") if menu_choice == 1: print_numbers(phone_list) elif menu_choice == 2: print "Add Name and Number" name = raw_input("Name:") phone = raw_input("Number:") add_number(phone_list,name,phone) elif menu_choice == 3: print "Remove Name and Number" name = raw_input("Name:") remove_number(phone_list,name) elif menu_choice == 4: print "Lookup Number" name = raw_input("Name:") print lookup_number(phone_list,name) elif menu_choice == 5: filename = raw_input("Filename to load:") load_numbers(phone_list,filename) elif menu_choice == 6: filename = raw_input("Filename to save:") save_numbers(phone_list,filename) elif menu_choice == 7: pass else: print_menu() print "Goodbye" </pre> 이제 그것은 파일을 저장하고 로드하는 것을 포함한다. 여기에 내가 그것을 두번 실행한 출력이 있다. <pre> > python tele2.py 1. Print Phone Numbers 2. Add a Phone Number 3. Remove a Phone Number 4. Lookup a Phone Number 5. Load numbers 6. Save numbers 7. Quit Type in a number (1-7):2 Add Name and Number Name:Jill Number:1234 Type in a number (1-7):2 Add Name and Number Name:Fred Number:4321 Type in a number (1-7):1 Telephone Numbers: Name: Jill Number: 1234 Name: Fred Number: 4321 Type in a number (1-7):6 Filename to save:numbers.txt Type in a number (1-7):7 Goodbye > python tele2.py 1. Print Phone Numbers 2. Add a Phone Number 3. Remove a Phone Number 4. Lookup a Phone Number 5. Load numbers 6. Save numbers 7. Quit Type in a number (1-7):5 Filename to load:numbers.txt Type in a number (1-7):1 Telephone Numbers: Name: Jill Number: 1234 Name: Fred Number: 4321 Type in a number (1-7):7 Goodbye </pre> 이 프로그램의 새로운 부분은 다음과 같다. <pre> def load_numbers(numbers,filename): in_file = open(filename,"r") while 1: in_line = in_file.readline() if len(in_line) == 0: break in_line = in_line[:-1] [name,number] = string.split(in_line,",") numbers[name] = number in_file.close() def save_numbers(numbers,filename): out_file = open(filename,"w") for x in numbers.keys(): out_file.write(x+","+numbers[x]+"\n") out_file.close() </pre> 먼저 우리는 그 프로그램에서 저장하는 부분을 살펴 볼 것이다. 먼저 그것은 open(filename,"w") 명령어로 파일 객체를 생성한다. 다음으로 그것은 전화번호들 각각을 위하여 'out_file.write(x+","+numbers[x]+"\n")' 명령어로 하나의 라인을 생성해 나간다. 이것은 이름, 컴마, 전화번호를 포함하는 하나의 라인을 출력하고 다음에 새로운 라인이 출력된다. 읽어들이는 부분은 약간은 더 복잡하다. 그것은 파일 객체를 획득하는 것으로 시작한다. 그리고 그것은 break서술문을 만날 때까지 회돌이를 유지하기 위하여 'while 1:'을 사용한다. 다음으로 in_line = in_file.readline()라는 코드로 한 라인을 읽어 들인다. getline함수는 파일의 끝에 다다르면 빈 문자열(len(string) == 0)을 반환할 것이다. if 서술문은 이것을 점검해서 만일 그런 일이 일어난다면 while회돌이를 빠져나온다. 물론 readline 함수가 새로운라인 문자를 그 라인의 끝에서 반환하지 않았다면 빈 문자열이 빈 라인인지 혹은 파일의 마지막인지 말해줄 방법이 없으므로 새로운라인 문자는 getline 이 반환해준 곳에 남는다. 그러므로 우리는 그 새로운라인 문자를 제거해야 한다. in_line = in_line[:-1] 라인은 우리를 위하여 마지막 문자를 제거 함으로써 이일을 해준다. 다음의 [name,number] = string.split(in_line,",") 라인은 컴마위치에서 이름과 전화번호로 그 라인을 분할한다. 그리고나서 이것은 numbers 사전에 등록된다. rwh3fue86uvkany4wt5opf298ghv379 일반인을 위한 파이썬 지침서/불완전을 다루기(또는 에러처리법) 0 11993 45165 2024-10-31T11:44:51Z Vpark45 8370 새 문서: 자, 이제 여러분은 완벽한 프로그램을 가지고 있으며, 한가지 세부사항만 빼고는, 그것은 결점없이 실행된다. 그것은 사용자의 무효한 입력에 충돌을 일으킬 것이다. 걱정하지 마라, 왜냐하면 파이썬은 여러분을 위하여 특별한 제어 구조를 구지고 있기 때문이다. 그것을 일컬어 try라고 부르며 그것은 뭔가를 하려고 시도한다. 여기에 한가지 문제점을 가지고 있는... 45165 wikitext text/x-wiki 자, 이제 여러분은 완벽한 프로그램을 가지고 있으며, 한가지 세부사항만 빼고는, 그것은 결점없이 실행된다. 그것은 사용자의 무효한 입력에 충돌을 일으킬 것이다. 걱정하지 마라, 왜냐하면 파이썬은 여러분을 위하여 특별한 제어 구조를 구지고 있기 때문이다. 그것을 일컬어 try라고 부르며 그것은 뭔가를 하려고 시도한다. 여기에 한가지 문제점을 가지고 있는 프로그램의 예제가 있다. <pre> print "Type Control C or -1 to exit" number = 1 while number != -1: number = int(raw_input("Enter a number: ")) print "You entered: ",number </pre> 여러분이 @#&를 입력해 넣을 때 프로그램이 다음과 같은 출력을 보이는 것을 주목하라. <pre> Traceback (innermost last): File "try_less.py", line 4, in ? number = int(raw_input("Enter a number: ")) ValueError: invalid literal for int(): @#& </pre> 여러분이 볼 수 있듯이 int 함수는 숫자 @#&에 불행해 한다 (물론 틀림없이 그럴거다). 마지막 라인을 보면 그 프로그램이 무엇인지 알 수 있다; 파이썬은 ValueError 를 발견했다. 어떻게 우리의 프로그램이 이것을 다룰 수 있을까? 우리가 먼저 해야 할일은 : 에러가 발생한 지점을 try 블록에 집어 넣는다, 그리고 둘째로 : 파이썬에게 ValueError들을 다루는 법을 가르쳐 준다. 다음의 프로그램이 이 일을 한다. <pre> print "Type Control C or -1 to exit" number = 1 while number != -1: try: number = int(raw_input("Enter a number: ")) print "You entered: ",number except ValueError: print "That was not a number." </pre> 이제 우리가 새로운 프로그램을 실행하고 거기에다 @#&를 주면 프로그램은 우리에게 ``That was not a number.''라고 말해주고서는 그전에 자신이 하던 작업을 계속한다. 여러분의 프로그램이 어떤 에러들을 계속 가지고 있고 여러분이 다루는 법을 알고 있다면, try 블록에 코드를 집어 넣어라, 그리고 그 에러를 처리하는 방법을 except블록에 집어 넣어라. 6h9ggiynxonueenflgitkcn7oldqitz 일반인을 위한 파이썬 지침서/마지막 0 11994 45166 2024-10-31T11:45:16Z Vpark45 8370 새 문서: 나는 지금 이 문서에 더 많은 섹션을 추가하는 일을 하고 있다. 지금 현재 나는 귀도 반 로섬이 쓴 파이썬 지침서를 (여러분이) 보기를 권한다. 여러분은 틀림없이 파이썬에 관하여 상당히 많이 이해를 하게 될 것이다. 이 지침서는 앞으로 더 많은 작업을 하게 될 것이다. 나는 여러분의 어떠한 비평이라도 환영하며 여러분은 원하시는 바의 사양에 대해 비평해 주시... 45166 wikitext text/x-wiki 나는 지금 이 문서에 더 많은 섹션을 추가하는 일을 하고 있다. 지금 현재 나는 귀도 반 로섬이 쓴 파이썬 지침서를 (여러분이) 보기를 권한다. 여러분은 틀림없이 파이썬에 관하여 상당히 많이 이해를 하게 될 것이다. 이 지침서는 앞으로 더 많은 작업을 하게 될 것이다. 나는 여러분의 어떠한 비평이라도 환영하며 여러분은 원하시는 바의 사양에 대해 비평해 주시기 바란다. 전자메일을 나에게 보내셔서 방대한 분량의 일을 완성하고, 개선하고, 그리고 비평들에 대해 더욱 주의를 기울이게 하여 주시면 감사하겠다 :) 기분좋은 프로그래밍은, 여러분의 인생을 그리고 어쩌면 세계를 바꿀지도 모를 일이다. 하고자 하는바=[ '에러들','모듈을 만드는 방법','루프에 대하여 더 자세히','문자열에 대하여 더 자세히', '파일 입출력','온라인 도움말을 사용하는 법','try문','pickle모듈','여러분이 제안하고 내가 보기에 좋은 생각이라고 여겨지는 모든 것'] i4hwxdrs6xti9cgudfbrwowvrqzuqj7 일반인을 위한 파이썬 지침서/FAQ 0 11995 45167 2024-10-31T11:46:42Z Vpark45 8370 새 문서: ;input 문을 가지고 프로그램을 쓸 수 없어요.:여러분이 IDLE를 사용하고 있다면 명령어 라인을 시도해 보세요. 이 문제는 IDLE 0.6 그 이상에서는 해결된거로 보입니다. 여러분이 구형 버젼을 사용하고 있다면 파이썬 2.0이나 더 신버젼으로 업그레이드 하세요. ;인쇄가 가능한 버젼이 있습니까?:네, 다음 질문을 보세요. ;PDF 혹은 zip으로 압축된 버젼이 있나요?;네, http://ww... 45167 wikitext text/x-wiki ;input 문을 가지고 프로그램을 쓸 수 없어요.:여러분이 IDLE를 사용하고 있다면 명령어 라인을 시도해 보세요. 이 문제는 IDLE 0.6 그 이상에서는 해결된거로 보입니다. 여러분이 구형 버젼을 사용하고 있다면 파이썬 2.0이나 더 신버젼으로 업그레이드 하세요. ;인쇄가 가능한 버젼이 있습니까?:네, 다음 질문을 보세요. ;PDF 혹은 zip으로 압축된 버젼이 있나요?;네, http://www.honors.montana.edu/~jjc/easytut/ 로 가시면 몇가지의 다른 버젼을 보실 수 있습니다. ;이 지침서는 무엇으로 작성하셨나요?:LATEX 입니다, easytut.tex 파일을 보세요. ;내 질문에 대한 답이 여기에 없네요.:나에게 전자메일을 보내 물어보세요. 여러분이 무엇을 하려고 하였는지, 무슨일이 일어났는지, 여러분은 무엇을 예상했는지, 에러 메시지, 파이썬의 버젼, (사용중인) 운영체제, 그리고 여러분의 고양이가 키보드 위를 뛰어 다니지는 않았는지, 등등을 첨부하여 주시면 도움이 되겠습니다. (우리집 고양이는 스페이스 바와 콘트롤 키를 좋아한답니다.) ;가장 마지막으로 언제 변경되었는지 그리고 무엇이 변경되었는지요?: * 2000-Dec-16, 에러 처리의 장이 추가되었습니다. * 2000-Dec-22, 구형 설치방법이 삭제 되었습니다. * 2001-Jan-16, 프로그램의 버그가 고쳐졌고, 리스트 섹션에 예제와 데이타를 추가 하였습니다. * 2001-Apr-5, 철자, 문법, 프로그램을 분석하는 법이 추가되었습니다, PDF 버젼의 url버그를 고쳤습니다. * 2001-May-13, 디버깅에 관한 장이 추가되었습니다. mjizb7ibdgq38pgn1vqejwoyagf0rqk 45168 45167 2024-10-31T11:47:10Z Vpark45 8370 45168 wikitext text/x-wiki ;input 문을 가지고 프로그램을 쓸 수 없어요.:여러분이 IDLE를 사용하고 있다면 명령어 라인을 시도해 보세요. 이 문제는 IDLE 0.6 그 이상에서는 해결된거로 보입니다. 여러분이 구형 버젼을 사용하고 있다면 파이썬 2.0이나 더 신버젼으로 업그레이드 하세요. ;인쇄가 가능한 버젼이 있습니까?:네, 다음 질문을 보세요. ;PDF 혹은 zip으로 압축된 버젼이 있나요?:네, http://www.honors.montana.edu/~jjc/easytut/ 로 가시면 몇가지의 다른 버젼을 보실 수 있습니다. ;이 지침서는 무엇으로 작성하셨나요?:LATEX 입니다, easytut.tex 파일을 보세요. ;내 질문에 대한 답이 여기에 없네요.:나에게 전자메일을 보내 물어보세요. 여러분이 무엇을 하려고 하였는지, 무슨일이 일어났는지, 여러분은 무엇을 예상했는지, 에러 메시지, 파이썬의 버젼, (사용중인) 운영체제, 그리고 여러분의 고양이가 키보드 위를 뛰어 다니지는 않았는지, 등등을 첨부하여 주시면 도움이 되겠습니다. (우리집 고양이는 스페이스 바와 콘트롤 키를 좋아한답니다.) ;가장 마지막으로 언제 변경되었는지 그리고 무엇이 변경되었는지요?: * 2000-Dec-16, 에러 처리의 장이 추가되었습니다. * 2000-Dec-22, 구형 설치방법이 삭제 되었습니다. * 2001-Jan-16, 프로그램의 버그가 고쳐졌고, 리스트 섹션에 예제와 데이타를 추가 하였습니다. * 2001-Apr-5, 철자, 문법, 프로그램을 분석하는 법이 추가되었습니다, PDF 버젼의 url버그를 고쳤습니다. * 2001-May-13, 디버깅에 관한 장이 추가되었습니다. 17jgcr17dcm0vekcs2levh21k550fql 45169 45168 2024-10-31T11:47:39Z Vpark45 8370 45169 wikitext text/x-wiki ;input 문을 가지고 프로그램을 쓸 수 없어요.:여러분이 IDLE를 사용하고 있다면 명령어 라인을 시도해 보세요. 이 문제는 IDLE 0.6 그 이상에서는 해결된거로 보입니다. 여러분이 구형 버젼을 사용하고 있다면 파이썬 2.0이나 더 신버젼으로 업그레이드 하세요. ;인쇄가 가능한 버젼이 있습니까?:네, 다음 질문을 보세요. ;PDF 혹은 zip으로 압축된 버젼이 있나요?:네, http://www.honors.montana.edu/~jjc/easytut/ 로 가시면 몇가지의 다른 버젼을 보실 수 있습니다. ;이 지침서는 무엇으로 작성하셨나요?:LATEX 입니다, easytut.tex 파일을 보세요. ;내 질문에 대한 답이 여기에 없네요.:나에게 전자메일을 보내 물어보세요. 여러분이 무엇을 하려고 하였는지, 무슨일이 일어났는지, 여러분은 무엇을 예상했는지, 에러 메시지, 파이썬의 버젼, (사용중인) 운영체제, 그리고 여러분의 고양이가 키보드 위를 뛰어 다니지는 않았는지, 등등을 첨부하여 주시면 도움이 되겠습니다. (우리집 고양이는 스페이스 바와 콘트롤 키를 좋아한답니다.) ;가장 마지막으로 언제 변경되었는지 그리고 무엇이 변경되었는지요?:<nowiki> </nowiki>* 2000-Dec-16, 에러 처리의 장이 추가되었습니다. * 2000-Dec-22, 구형 설치방법이 삭제 되었습니다. * 2001-Jan-16, 프로그램의 버그가 고쳐졌고, 리스트 섹션에 예제와 데이타를 추가 하였습니다. * 2001-Apr-5, 철자, 문법, 프로그램을 분석하는 법이 추가되었습니다, PDF 버젼의 url버그를 고쳤습니다. * 2001-May-13, 디버깅에 관한 장이 추가되었습니다. czze4sgbbrhurm75a13goe1529k9fkf 45170 45169 2024-10-31T11:47:57Z Vpark45 8370 45170 wikitext text/x-wiki ;input 문을 가지고 프로그램을 쓸 수 없어요.:여러분이 IDLE를 사용하고 있다면 명령어 라인을 시도해 보세요. 이 문제는 IDLE 0.6 그 이상에서는 해결된거로 보입니다. 여러분이 구형 버젼을 사용하고 있다면 파이썬 2.0이나 더 신버젼으로 업그레이드 하세요. ;인쇄가 가능한 버젼이 있습니까?:네, 다음 질문을 보세요. ;PDF 혹은 zip으로 압축된 버젼이 있나요?:네, http://www.honors.montana.edu/~jjc/easytut/ 로 가시면 몇가지의 다른 버젼을 보실 수 있습니다. ;이 지침서는 무엇으로 작성하셨나요?:LATEX 입니다, easytut.tex 파일을 보세요. ;내 질문에 대한 답이 여기에 없네요.:나에게 전자메일을 보내 물어보세요. 여러분이 무엇을 하려고 하였는지, 무슨일이 일어났는지, 여러분은 무엇을 예상했는지, 에러 메시지, 파이썬의 버젼, (사용중인) 운영체제, 그리고 여러분의 고양이가 키보드 위를 뛰어 다니지는 않았는지, 등등을 첨부하여 주시면 도움이 되겠습니다. (우리집 고양이는 스페이스 바와 콘트롤 키를 좋아한답니다.) ;가장 마지막으로 언제 변경되었는지 그리고 무엇이 변경되었는지요? * 2000-Dec-16, 에러 처리의 장이 추가되었습니다. * 2000-Dec-22, 구형 설치방법이 삭제 되었습니다. * 2001-Jan-16, 프로그램의 버그가 고쳐졌고, 리스트 섹션에 예제와 데이타를 추가 하였습니다. * 2001-Apr-5, 철자, 문법, 프로그램을 분석하는 법이 추가되었습니다, PDF 버젼의 url버그를 고쳤습니다. * 2001-May-13, 디버깅에 관한 장이 추가되었습니다. it61838pejpp9iwbce9bp8zmb6k2phj 일반인을 위한 파이썬 지침서/이 문서에 대하여... 0 11996 45171 2024-10-31T11:48:54Z Vpark45 8370 새 문서: 이 문서는 LaTeX2HTML translator Version 99.2beta6 (1.42)를 사용하여 작성되어졌다. Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds. Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney. 명렁어 라인의 인수는 다음과 같다. <pre> latex2html -split 3 -local_icons -address 'Josh Cogliati jjc@iname.com' easytut.tex </pre> 번역은 Josh Cogliati 가 2001-05-13... 45171 wikitext text/x-wiki 이 문서는 LaTeX2HTML translator Version 99.2beta6 (1.42)를 사용하여 작성되어졌다. Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds. Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney. 명렁어 라인의 인수는 다음과 같다. <pre> latex2html -split 3 -local_icons -address 'Josh Cogliati jjc@iname.com' easytut.tex </pre> 번역은 Josh Cogliati 가 2001-05-13 에 처음했다. re0bo8ofhf49mnntr3nceybwv2196mb