การเรียนรู้เอล์มจากตัวจัดลำดับกลอง (ตอนที่ 1)
เผยแพร่แล้ว: 2022-03-10หากคุณเป็นนักพัฒนาส่วนหน้าตามวิวัฒนาการของแอปพลิเคชันหน้าเดียว (SPA) คุณคงเคยได้ยินชื่อ Elm ซึ่งเป็นภาษาที่ใช้งานได้จริงซึ่งเป็นแรงบันดาลใจให้ Redux หากคุณยังไม่มี มันเป็นภาษาที่คอมไพล์เป็น JavaScript ที่เทียบได้กับโปรเจ็กต์ SPA เช่น React, Angular และ Vue
เช่นเดียวกับสิ่งเหล่านี้ มันจัดการการเปลี่ยนแปลงสถานะผ่านโดมเสมือนที่มีจุดมุ่งหมายเพื่อให้โค้ดสามารถบำรุงรักษาและทำงานได้อย่างมีประสิทธิภาพมากขึ้น โดยเน้นไปที่ความสุขของนักพัฒนา เครื่องมือคุณภาพสูง และรูปแบบที่เรียบง่ายและทำซ้ำได้ ความแตกต่างที่สำคัญบางประการ ได้แก่ การพิมพ์แบบคงที่ ข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์อย่างยิ่ง และเป็นภาษาที่ใช้งานได้ (ตรงข้ามกับ Object-Oriented)
การแนะนำของฉันมาจากการพูดคุยของ Evan Czaplicki ผู้สร้าง Elm เกี่ยวกับวิสัยทัศน์ของเขาสำหรับประสบการณ์ของนักพัฒนาส่วนหน้า และในทางกลับกัน ก็คือวิสัยทัศน์สำหรับ Elm ในขณะที่บางคนให้ความสำคัญกับการบำรุงรักษาและความสามารถในการใช้งานของการพัฒนาส่วนหน้า คำพูดของเขาก็โดนใจฉันมาก ฉันลองใช้ Elm ในโปรเจ็กต์ย่อยเมื่อหนึ่งปีที่แล้ว และยังคงสนุกกับฟีเจอร์และความท้าทายต่างๆ ต่อไปในแบบที่ฉันไม่ได้ทำตั้งแต่เริ่มเขียนโปรแกรมครั้งแรก ฉันเป็นมือใหม่อีกครั้ง นอกจากนี้ ฉันยังพบว่าตนเองสามารถนำแนวทางปฏิบัติของ Elm ไปใช้กับภาษาอื่นๆ ได้
การพัฒนาการรับรู้การพึ่งพา
การพึ่งพามีอยู่ทุกที่ คุณสามารถปรับปรุงโอกาสที่ไซต์ของคุณจะใช้งานได้โดยผู้คนจำนวนมากที่สุดในสถานการณ์ที่หลากหลายที่สุด อ่านบทความที่เกี่ยวข้อง →
ในบทความสองตอนนี้ เราจะสร้างซีเควนเซอร์ขั้นตอนเพื่อตั้งโปรแกรมจังหวะกลองใน Elm พร้อมแสดงคุณลักษณะที่ดีที่สุดของภาษาบางส่วน วันนี้ เราจะอธิบายเกี่ยวกับแนวคิดพื้นฐานใน Elm เช่น การเริ่มต้นใช้งาน การใช้ประเภท การเรนเดอร์มุมมอง และการอัปเดตสถานะ ส่วนที่สองของบทความนี้จะเจาะลึกในหัวข้อขั้นสูง เช่น การจัดการตัวสร้างใหม่ขนาดใหญ่ได้อย่างง่ายดาย การตั้งค่าเหตุการณ์ที่เกิดซ้ำ และการโต้ตอบกับ JavaScript
เล่นกับโปรเจ็กต์สุดท้ายที่นี่ และดูโค้ดของโปรเจ็กต์ที่นี่
เริ่มต้นกับ Elm
สำหรับการติดตามในบทความนี้ ผมขอแนะนำให้ใช้ Ellie ซึ่งเป็นประสบการณ์สำหรับนักพัฒนา Elm ในเบราว์เซอร์ คุณไม่จำเป็นต้องติดตั้งอะไรเพื่อเรียกใช้ Ellie และคุณสามารถพัฒนาแอปพลิเคชันที่ทำงานได้อย่างสมบูรณ์ในนั้น หากคุณต้องการติดตั้ง Elm บนคอมพิวเตอร์ของคุณ วิธีที่ดีที่สุดในการตั้งค่าคือปฏิบัติตามคู่มือเริ่มต้นใช้งานอย่างเป็นทางการ
ตลอดบทความนี้ ฉันจะลิงก์ไปยังเวอร์ชันของ Ellie ที่กำลังดำเนินการอยู่ แม้ว่าฉันจะพัฒนาซีเควนเซอร์ในเครื่องแล้วก็ตาม และในขณะที่ CSS สามารถเขียนได้ทั้งหมดใน Elm ฉันได้เขียนโปรเจ็กต์นี้ใน PostCSS สิ่งนี้ต้องการการกำหนดค่าเล็กน้อยให้กับ Elm Reactor สำหรับการพัฒนาในพื้นที่เพื่อให้โหลดสไตล์ได้ เพื่อความกระชับ ฉันจะไม่พูดถึงสไตล์ในบทความนี้ แต่ลิงก์ของ Ellie จะรวมสไตล์ CSS ที่ย่อเล็กสุดไว้ทั้งหมด
Elm เป็นระบบนิเวศที่มีอยู่ในตัวเองซึ่งรวมถึง:
- เอล์มเมค
สำหรับการรวบรวมรหัส Elm ของคุณ แม้ว่า Webpack จะยังคงเป็นที่นิยมสำหรับการผลิตโปรเจ็กต์ Elm ควบคู่ไปกับสินทรัพย์อื่นๆ แต่ก็ไม่จำเป็น ในโครงการนี้ ฉันเลือกที่จะไม่รวม Webpack และใช้elm make
เพื่อคอมไพล์โค้ด - แพ็คเกจเอล์ม
ตัวจัดการแพ็คเกจเทียบได้กับ NPM สำหรับการใช้แพ็คเกจ/โมดูลที่สร้างโดยชุมชน - เครื่องปฏิกรณ์เอล์ม
สำหรับการรันเซิร์ฟเวอร์การพัฒนาที่คอมไพล์โดยอัตโนมัติ ที่โดดเด่นกว่านั้นคือมีโปรแกรมแก้ไขข้อบกพร่องการเดินทางข้ามเวลาซึ่งทำให้ง่ายต่อการตรวจสอบสถานะแอปพลิเคชันของคุณและจุดบกพร่องใน การเล่นซ้ำ - Elm Repl
สำหรับการเขียนหรือทดสอบนิพจน์ Elm อย่างง่ายในเทอร์มินัล
ไฟล์ Elm ทั้งหมดถือเป็น modules
บรรทัดเริ่มต้นของไฟล์ใดๆ จะรวม module FileName exposing (functions)
โดยที่ FileName
เป็นชื่อไฟล์ตามตัวอักษร และ functions
คือฟังก์ชันสาธารณะที่คุณต้องการให้โมดูลอื่นๆ เข้าถึงได้ ทันทีหลังจากที่มีการนำเข้าคำจำกัดความของโมดูลจากโมดูลภายนอก ฟังก์ชั่นที่เหลือจะตามมา
module Main exposing (main) import Html exposing (Html, text) main : Html msg main = text "Hello, World!"
โมดูลนี้ชื่อ Main.elm
แสดงฟังก์ชันเดียว main
และนำเข้า Html
และ text
จากโมดูล/แพ็คเกจ Html
ฟังก์ชัน main
ประกอบด้วยสองส่วน: ประเภทคำอธิบายประกอบ และฟังก์ชันจริง คำอธิบายประกอบประเภทถือเป็นคำจำกัดความของฟังก์ชัน พวกเขาระบุประเภทอาร์กิวเมนต์และประเภทการส่งคืน ในกรณีนี้ เราระบุว่าฟังก์ชัน main
ไม่มีอาร์กิวเมนต์และส่งคืน Html msg
ฟังก์ชันนี้สร้างโหนดข้อความที่มีคำว่า "สวัสดี โลก" ในการส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชัน เราเพิ่มชื่อที่คั่นด้วยช่องว่าง ก่อน เครื่องหมายเท่ากับในฟังก์ชัน เรายังเพิ่มประเภทอาร์กิวเมนต์ให้กับประเภทคำอธิบายประกอบ ตามลำดับอาร์กิวเมนต์ ตามด้วยลูกศร
add2Numbers : Int -> Int -> Int add2Numbers first second = first + second
ใน JavaScript ฟังก์ชันเช่นนี้เปรียบได้:
function add2Numbers(first, second) { return first + second; }
และในภาษา Typed เช่น TypeScript ดูเหมือนว่า:
function add2Numbers(first: number, second: number): number { return first + second; }
add2Numbers
รับจำนวนเต็มสองจำนวนและส่งกลับจำนวนเต็ม ค่าสุดท้ายในคำอธิบายประกอบจะเป็นค่าที่ส่งคืนเสมอ เนื่องจากทุกฟังก์ชัน ต้อง คืนค่า เราเรียก add2Numbers
ด้วย 2 และ 3 เพื่อรับ 5 เช่น add2Numbers 2 3
เช่นเดียวกับที่คุณผูกส่วนประกอบ React เราจำเป็นต้องผูกโค้ด Elm ที่คอมไพล์แล้วเข้ากับ DOM วิธีมาตรฐานในการผูกคือการเรียก embed()
บนโมดูลของเราและส่งผ่านองค์ประกอบ DOM เข้าไป
<script> const container = document.getElementById('app'); const app = Elm.Main.embed(container); <script>
แม้ว่าแอปของเราไม่ได้ทำอะไรเลยจริงๆ แต่เราก็มีเพียงพอที่จะรวบรวมโค้ด Elm และแสดงข้อความ ลองใช้ Ellie แล้วลองเปลี่ยนอาร์กิวเมนต์เป็น add2Numbers
ในบรรทัดที่ 26
การสร้างแบบจำลองข้อมูลด้วยประเภท
มาจากภาษาที่พิมพ์แบบไดนามิกเช่น JavaScript หรือ Ruby ประเภทอาจดูเหมือนไม่จำเป็น ภาษาเหล่านั้นกำหนดประเภทของฟังก์ชันที่ใช้จากค่าที่ส่งผ่าน ระหว่าง รันไทม์ การเขียนฟังก์ชันโดยทั่วไปถือว่าเร็วกว่า แต่คุณจะสูญเสียความปลอดภัยในการทำให้มั่นใจว่าฟังก์ชันของคุณสามารถโต้ตอบกันได้อย่างเหมาะสม
ในทางตรงกันข้าม Elm ถูกพิมพ์แบบคงที่ มันอาศัยคอมไพเลอร์เพื่อให้แน่ใจว่าค่าที่ส่งไปยังฟังก์ชันนั้นเข้ากันได้ ก่อน รันไทม์ ซึ่งหมายความว่าไม่มีข้อยกเว้นรันไทม์สำหรับผู้ใช้ของคุณและเป็นวิธีที่ Elm สามารถรับประกัน "ไม่มีข้อยกเว้นรันไทม์" ได้ ในกรณีที่ข้อผิดพลาดของประเภทในคอมไพเลอร์จำนวนมากสามารถมีความคลุมเครือโดยเฉพาะอย่างยิ่ง Elm มุ่งเน้นที่การทำให้เข้าใจและแก้ไขได้ง่าย
Elm ทำให้การเริ่มต้นใช้งานประเภทที่เป็นมิตรมาก อันที่จริง การอนุมานประเภทของ Elm นั้นดีมาก คุณสามารถข้ามการเขียนคำอธิบายประกอบได้จนกว่าคุณจะคุ้นเคยกับคำอธิบายประกอบมากขึ้น หากคุณเพิ่งเริ่มใช้ประเภท ฉันขอแนะนำให้ใช้คำแนะนำของคอมไพเลอร์แทนที่จะพยายามเขียนด้วยตัวเอง
มาเริ่มสร้างแบบจำลองข้อมูลของเราโดยใช้ประเภทกันเถอะ ซีเควนเซอร์ขั้นตอนของเราคือไทม์ไลน์ที่แสดงให้เห็นเวลาที่ตัวอย่างกลองควรเล่น ไทม์ไลน์ประกอบด้วย แทร็ก โดยแต่ละแทร็กจะกำหนดตัวอย่างดรัมเฉพาะและ ลำดับขั้นตอน ก้าว สามารถถือเป็นช่วงเวลาหรือจังหวะ หากขั้นตอน ทำงานอยู่ ตัวอย่างควรถูกทริกเกอร์ในระหว่างการเล่น และหากขั้นตอนนั้น ไม่ได้ใช้งาน ตัวอย่างควรเงียบไว้ ในระหว่างการเล่น ซีเควนเซอร์จะเคลื่อนผ่านแต่ละขั้นตอนโดยเล่นตัวอย่างของขั้นตอนที่ทำงานอยู่ ความเร็วในการเล่นถูกกำหนดโดย Beats Per Minute (BPM)
การสร้างแบบจำลองแอปพลิเคชันของเราใน JavaScript
เพื่อให้ได้แนวคิดที่ดีขึ้นเกี่ยวกับประเภทของเรา มาพิจารณาถึงวิธีการสร้างโมเดลดรัมซีเควนเซอร์ใน JavaScript มีอาร์เรย์ของแทร็ก แต่ละออบเจ็กต์แทร็กประกอบด้วยข้อมูลเกี่ยวกับตัวเอง: ชื่อแทร็ก ตัวอย่าง/คลิปที่จะทริกเกอร์ และลำดับของค่าขั้นตอน
tracks: [ { name: "Kick", clip: "kick.mp3", sequence: [On, Off, Off, Off, On, etc...] }, { name: "Snare", clip: "snare.mp3", sequence: [Off, Off, Off, Off, On, etc...] }, etc... ]
เราจำเป็นต้องจัดการสถานะการเล่นระหว่างการเล่นและการหยุด
playback: "playing" || "stopped"
ระหว่างการเล่น เราจำเป็นต้องกำหนดขั้นตอนที่ควรเล่น เราควรพิจารณาประสิทธิภาพการเล่นด้วย และแทนที่จะข้ามแต่ละลำดับในแต่ละแทร็กทุกครั้งที่มีการเพิ่มทีละขั้น เราควรลดขั้นตอนการทำงานทั้งหมดลงในลำดับการเล่นเดียว แต่ละคอลเลกชันภายในลำดับการเล่นแสดงถึงตัวอย่างทั้งหมดที่ควรเล่น ตัวอย่างเช่น ["kick", "hat"]
หมายถึงตัวอย่างการเตะและ hi-hat ควรเล่น ในขณะที่ ["hat"]
หมายถึงเฉพาะ hi-hat เท่านั้นที่ควรเล่น เรายังต้องการให้แต่ละคอลเลกชันจำกัดความเป็นเอกลักษณ์ให้กับกลุ่มตัวอย่าง ดังนั้นเราจึงไม่ลงเอยด้วยบางอย่างเช่น ["hat", "hat", "hat"]
playbackPosition: 1 playbackSequence: [ ["kick", "hat"], [], ["hat"], [], ["snare", "hat"], [], ["hat"], [], ... ],
และเราต้องกำหนดความเร็วในการเล่นหรือ BPM
bpm: 120
การสร้างแบบจำลองด้วยประเภทใน Elm
การถ่ายทอดข้อมูลนี้เป็นประเภท Elm เป็นการอธิบายสิ่งที่เราคาดหวังจากข้อมูลของเราเป็นหลัก ตัวอย่างเช่น เรากำลังอ้างถึงโมเดลข้อมูลของเราว่าเป็น model ดังนั้นเราจึงเรียกมันด้วยชื่อแทนประเภท นามแฝงประเภทใช้เพื่อทำให้โค้ดอ่านง่ายขึ้น ไม่ใช่ ประเภทดั้งเดิม เช่นบูลีนหรือจำนวนเต็ม เป็นเพียงชื่อที่เรากำหนดประเภทดั้งเดิมหรือโครงสร้างข้อมูล เมื่อใช้สิ่งนี้ เราจะกำหนดข้อมูลใดๆ ที่เป็นไปตามโครงสร้างโมเดลของเราเป็น แบบโมเดล แทนที่จะเป็นโครงสร้างที่ไม่ระบุตัวตน ในโครงการ Elm หลายโครงการ โครงสร้างหลักมีชื่อว่า Model
type alias Model = { tracks : Array Track , playback : Playback , playbackPosition : PlaybackPosition , bpm : Int , playbackSequence : Array (Set Clip) }
แม้ว่าโมเดลของเราจะดูเหมือนวัตถุ JavaScript แต่ก็อธิบาย Elm Record เร็กคอร์ดใช้เพื่อจัดระเบียบข้อมูลที่เกี่ยวข้องเป็นหลายฟิลด์ซึ่งมีคำอธิบายประกอบประเภทของตนเอง เข้าถึงได้ง่ายโดยใช้ field.attribute
และอัปเดตได้ง่ายซึ่งเราจะเห็นในภายหลัง ออบเจ็กต์และเร็กคอร์ดมีความคล้ายคลึงกันมาก โดยมีความแตกต่างที่สำคัญบางประการ:
- ฟิลด์ที่ไม่มีอยู่ไม่สามารถเรียกได้
- ฟิลด์จะไม่เป็น
null
หรือundefined
-
this
และself
ใช้ไม่ได้
คอลเลคชันแทร็กของเราประกอบด้วยหนึ่งในสามประเภทที่เป็นไปได้: รายการ อาร์เรย์ และชุด กล่าวโดยย่อ รายการคือคอลเล็กชันการใช้งานทั่วไปที่ไม่ได้จัดทำดัชนี อาร์เรย์ได้รับการจัดทำดัชนี และชุดประกอบด้วยค่าที่ไม่ซ้ำกันเท่านั้น เราจำเป็นต้องมีดัชนีเพื่อทราบว่ามีการสลับขั้นตอนของแทร็กใด และเนื่องจากอาร์เรย์ได้รับการจัดทำดัชนีแล้ว จึงเป็นตัวเลือกที่ดีที่สุดของเรา อีกทางหนึ่ง เราสามารถเพิ่ม id ให้กับแทร็กและกรองจากรายการ
ในแบบจำลองของเรา เราได้จัดเรียงแทร็กเป็นอาร์เรย์ของ แทร็ก อีกระเบียนหนึ่งคือ: tracks : Array Track
ติดตามมีข้อมูลเกี่ยวกับตัวเอง ทั้งชื่อและคลิปเป็นสตริง แต่เราพิมพ์ชื่อแทนคลิปเพราะเรารู้ว่าจะมีการอ้างอิงถึงที่อื่นในโค้ดโดยฟังก์ชันอื่น โดยการใช้นามแฝง เราจะเริ่มสร้างรหัสการจัดทำเอกสารด้วยตนเอง การสร้างประเภทและนามแฝงประเภทช่วยให้นักพัฒนาสามารถสร้างแบบจำลองข้อมูลกับโมเดลธุรกิจ สร้างภาษาที่แพร่หลาย
type alias Track = { name : String , clip : Clip , sequence : Array Step } type Step = On | Off type alias Clip = String
เรารู้ว่าลำดับจะเป็นอาร์เรย์ของค่าเปิด/ปิด เราสามารถตั้งค่าให้เป็นอาร์เรย์บูลีนได้ เช่น sequence : Array Bool
แต่เราจะพลาดโอกาสในการแสดงรูปแบบธุรกิจของเรา เมื่อพิจารณาว่าซีเควนเซอร์ขั้นตอนประกอบด้วย ขั้นตอน เราจึงกำหนดประเภทใหม่ที่เรียกว่า ขั้นตอน ขั้นตอนอาจเป็นชื่อแทนประเภทสำหรับ boolean
แต่เราสามารถก้าวไปอีกขั้นหนึ่งได้ ขั้นตอนมีค่าที่เป็นไปได้สองค่า เปิดและปิด นั่นคือวิธีที่เรากำหนดประเภทสหภาพ ตอนนี้ขั้นตอนสามารถเปิดหรือปิดได้เท่านั้น ทำให้สถานะอื่นทั้งหมดเป็นไปไม่ได้
เรากำหนดประเภทอื่นสำหรับ Playback
ซึ่งเป็นนามแฝงสำหรับ PlaybackPosition
และใช้ Clip เมื่อกำหนด playbackSequence
เป็น Array ที่มีชุดของ Clips BPM ถูกกำหนดให้เป็น Int
มาตรฐาน
type Playback = Playing | Stopped type alias PlaybackPosition = Int
แม้ว่าจะมีค่าใช้จ่ายเพิ่มเติมเล็กน้อยในการเริ่มต้นใช้งานประเภท แต่โค้ดของเราสามารถบำรุงรักษาได้ดีกว่ามาก เป็นการทำเอกสารด้วยตนเองและใช้ภาษาที่แพร่หลายกับโมเดลธุรกิจของเรา ความมั่นใจที่เราได้รับในการรู้หน้าที่ในอนาคตของเราจะโต้ตอบกับข้อมูลของเราในแบบที่เราคาดหวัง โดยไม่ต้องทำการทดสอบ จะคุ้มค่ากับเวลาที่ใช้ในการเขียนคำอธิบายประกอบ และเราสามารถพึ่งพาการอนุมานประเภทของคอมไพเลอร์เพื่อแนะนำประเภทต่างๆ ได้ ดังนั้นการเขียนจึงง่ายพอๆ กับการคัดลอกและวาง นี่คือการประกาศประเภทเต็ม
การใช้สถาปัตยกรรมเอล์ม
สถาปัตยกรรม Elm เป็นรูปแบบการจัดการสถานะอย่างง่ายที่เกิดขึ้นตามธรรมชาติในภาษา สร้างการมุ่งเน้นที่โมเดลธุรกิจและสามารถปรับขนาดได้สูง ตรงกันข้ามกับเฟรมเวิร์ก SPA อื่นๆ Elm ให้ความเห็นเกี่ยวกับสถาปัตยกรรม — เป็นวิธีการจัดโครงสร้างแอปพลิเคชันทั้งหมด ซึ่งทำให้การเริ่มต้นใช้งานเป็นเรื่องง่าย สถาปัตยกรรมประกอบด้วยสามส่วน:
- model ที่มี state ของ application และโครงสร้างที่เราพิมพ์ aliased model
- ฟังก์ชัน อัปเดต ซึ่งอัปเดตสถานะ
- และฟังก์ชั่น ดู ซึ่งทำให้สถานะเป็นภาพ
มาเริ่มสร้างซีเควนเซอร์กลองของเราเพื่อเรียนรู้เกี่ยวกับ Elm Architecture ในทางปฏิบัติกัน เราจะเริ่มต้นด้วยการเริ่มต้นแอปพลิเคชันของเรา แสดงมุมมอง จากนั้นอัปเดตสถานะของแอปพลิเคชัน มาจากพื้นหลังของ Ruby ฉันมักจะชอบไฟล์ที่สั้นกว่าและแยกฟังก์ชัน Elm ของฉันออกเป็นโมดูล แม้ว่าจะเป็นเรื่องปกติมากที่จะมีไฟล์ Elm ขนาดใหญ่ ฉันได้สร้างจุดเริ่มต้นบน Ellie แล้ว แต่ฉันได้สร้างไฟล์ต่อไปนี้ในเครื่อง:
- Types.elm มีคำจำกัดความประเภททั้งหมด
- Main.elm ซึ่งเริ่มต้นและรันโปรแกรม
- Update.elm ซึ่งมีฟังก์ชันการอัพเดทที่จัดการ state
- View.elm มีโค้ด Elm เพื่อแสดงผลเป็น HTML
การเริ่มต้นการสมัครของเรา
เป็นการดีที่สุดที่จะเริ่มต้นจากสิ่งเล็กๆ น้อยๆ ดังนั้นเราจึงลดโมเดลเพื่อมุ่งเน้นที่การสร้างแทร็กเดียวที่มีขั้นตอนที่สลับปิดและเปิด ในขณะที่เรา คิดว่าเรารู้ โครงสร้างข้อมูลทั้งหมดแล้ว การเริ่มต้นจากเล็กๆ น้อยๆ ช่วยให้เราสามารถมุ่งเน้นไปที่การแสดงแทร็กเป็น HTML มันลดความซับซ้อนและคุณไม่จำเป็นต้องใช้รหัส ต่อมา คอมไพเลอร์จะแนะนำเราตลอดการปรับโครงสร้างโมเดลของเรา ในไฟล์ Types.elm เราเก็บประเภทขั้นตอนและคลิปไว้ แต่เปลี่ยนรูปแบบและติดตาม
type alias Model = { track : Track } type alias Track = { name : String , sequence : Array Step } type Step = On | Off type alias Clip = String
ในการแสดง Elm เป็น HTML เราใช้แพ็คเกจ Elm Html มีตัวเลือกในการสร้างโปรแกรมสามประเภทที่สร้างขึ้นจากกันและกัน:
- โปรแกรมเริ่มต้น
โปรแกรมลดขนาดที่ไม่รวมผลข้างเคียงและมีประโยชน์อย่างยิ่งสำหรับการเรียนรู้สถาปัตยกรรม Elm - โปรแกรม
โปรแกรมมาตรฐานที่จัดการผลข้างเคียง ซึ่งมีประโยชน์สำหรับการทำงานกับฐานข้อมูลหรือเครื่องมือที่มีอยู่ภายนอก Elm - โปรแกรมพร้อมธง
โปรแกรมเสริมที่สามารถเริ่มต้นตัวเองด้วยข้อมูลจริงแทนข้อมูลเริ่มต้น
แนวทางปฏิบัติที่ดีคือการใช้โปรแกรมประเภทที่ง่ายที่สุดเพราะจะง่ายต่อการเปลี่ยนในภายหลังด้วยคอมไพเลอร์ นี่เป็นวิธีปฏิบัติทั่วไปเมื่อเขียนโปรแกรมใน Elm; ใช้เฉพาะสิ่งที่คุณต้องการและเปลี่ยนแปลงในภายหลัง สำหรับจุดประสงค์ของเรา เรารู้ว่าเราต้องจัดการกับ JavaScript ซึ่งถือว่าเป็นผลข้างเคียง ดังนั้นเราจึงสร้าง Html.program
ใน Main.elm เราจำเป็นต้องเริ่มต้นโปรแกรมโดยส่งฟังก์ชันไปยังฟิลด์ของโปรแกรม
main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = always Sub.none }
แต่ละฟิลด์ในโปรแกรมจะส่งฟังก์ชันไปยัง Elm Runtime ซึ่งควบคุมแอปพลิเคชันของเรา โดยสรุป Elm Runtime:
- เริ่มโปรแกรมด้วยค่าเริ่มต้นจาก
init
- แสดงมุมมองแรกโดยส่งโมเดลเริ่มต้นของเราไปใน
view
- แสดงมุมมองใหม่อย่างต่อเนื่องเมื่อมีการส่งข้อความเพื่อ
update
จากมุมมอง คำสั่ง หรือการสมัครรับข้อมูล
ภายในเครื่อง ฟังก์ชัน view
และ update
ของเราจะถูกนำเข้าจาก View.elm
และ Update.elm
ตามลำดับ และเราจะสร้างฟังก์ชันเหล่านั้นในอีกสักครู่ subscriptions
ฟังข้อความเพื่อทำให้เกิดการอัปเดต แต่สำหรับตอนนี้ เราเพิกเฉยต่อข้อความเหล่านั้นโดยมอบหมาย always Sub.none
ฟังก์ชันแรกของเรา init
เริ่มต้นโมเดล คิดว่า init
เป็นค่าเริ่มต้นสำหรับการโหลดครั้งแรก เรากำหนดมันด้วยแทร็กเดียวชื่อ "เตะ" และลำดับของขั้นตอนปิด เนื่องจากเราไม่ได้รับข้อมูลแบบอะซิงโครนัส เราจึงเพิกเฉยต่อคำสั่งด้วย Cmd.none
เพื่อเริ่มต้นโดยไม่มีผลข้างเคียง
init : ( Model, Cmd.Cmd Msg ) init = ( { track = { sequence = Array.initialize 16 (always Off) , name = "Kick" } } , Cmd.none )
คำอธิบายประกอบประเภทเริ่มต้นของเราตรงกับโปรแกรมของเรา เป็นโครงสร้างข้อมูลที่เรียกว่าทูเพิล ซึ่งมีค่าจำนวนคงที่ ในกรณีของเรา Model
และคำสั่ง สำหรับตอนนี้ เรามักจะละเลยคำสั่งโดยใช้ Cmd.none
จนกว่าเราจะพร้อมที่จะจัดการกับผลข้างเคียงในภายหลัง แอพของเราไม่แสดงผลอะไรเลย แต่มันรวบรวม!
การแสดงผลแอปพลิเคชันของเรา
มาสร้างมุมมองของเรากันเถอะ ณ จุดนี้ โมเดลของเรามีแทร็กเดียว นั่นคือสิ่งเดียวที่เราต้องแสดง โครงสร้าง HTML ควรมีลักษณะดังนี้:
<div class="track"> <p class "track-title">Kick</p> <div class="track-sequence"> <button class="step _active"></button> <button class="step"></button> <button class="step"></button> <button class="step"></button> etc... </div> </div>
เราจะสร้างสามฟังก์ชันเพื่อแสดงมุมมองของเรา:
- หนึ่งรายการเพื่อแสดงแทร็กเดียว ซึ่งประกอบด้วยชื่อแทร็กและซีเควนซ์
- อื่นเพื่อแสดงลำดับตัวเอง
- และอีกอันหนึ่งเพื่อแสดงปุ่มแต่ละขั้นตอนภายในลำดับ
ฟังก์ชันมุมมองแรกของเราจะแสดงแทร็กเดียว เราใช้คำอธิบายประกอบประเภทของเรา renderTrack : Track -> Html Msg
เพื่อบังคับใช้แทร็กเดียวที่ส่งผ่าน การใช้ประเภทหมายความว่าเรารู้ อยู่เสมอ ว่า renderTrack
จะมีแทร็ก เราไม่จำเป็นต้องตรวจสอบว่ามีฟิลด์ name
อยู่ในเรกคอร์ดหรือไม่ หรือถ้าเราส่งผ่านสตริงแทนที่จะเป็นเรกคอร์ด Elm จะไม่คอมไพล์หากเราพยายามส่งอย่างอื่นที่ไม่ใช่ Track
to renderTrack
ยิ่งไปกว่านั้น หากเราทำผิดพลาดและพยายามส่งอย่างอื่นที่ไม่ใช่แทร็กไปยังฟังก์ชันโดยไม่ได้ตั้งใจ คอมไพเลอร์จะให้ข้อความที่เป็นมิตรแก่เราเพื่อชี้ให้เราไปในทิศทางที่ถูกต้อง
renderTrack : Track -> Html Msg renderTrack track = div [ class "track" ] [ p [ class "track-title" ] [ text track.name ] , div [ class "track-sequence" ] (renderSequence track.sequence) ]
อาจดูเหมือนชัดเจน แต่ Elm ทั้งหมดเป็น Elm รวมถึงการเขียน HTML ไม่มีภาษาแม่แบบหรือสิ่งที่เป็นนามธรรมในการเขียน HTML — มันคือ Elm ทั้งหมด องค์ประกอบ HTML คือฟังก์ชัน Elm ซึ่งใช้ชื่อ รายการแอตทริบิวต์ และรายการลูก ดังนั้น div [ class "track" ] []
outputs <div class="track"></div>
รายการต่างๆ คั่นด้วยเครื่องหมายจุลภาคใน Elm ดังนั้นการเพิ่ม id ให้กับ div จะดูเหมือน div [ class "track", id "my-id" ] []
ลำดับแทร็กที่ตัด div ส่ง track-sequence
ของแทร็กไปยังฟังก์ชันที่สองของเรา renderSequence
ใช้ลำดับและส่งคืนรายการปุ่ม HTML เราสามารถเก็บ renderSequence
ใน renderTrack
เพื่อข้ามฟังก์ชันเพิ่มเติมได้ แต่ฉันพบว่าการแยกฟังก์ชันออกเป็นชิ้นเล็ก ๆ นั้นง่ายกว่ามากในการให้เหตุผล นอกจากนี้ เรายังได้รับโอกาสอีกครั้งในการกำหนดคำอธิบายประกอบประเภทที่เข้มงวดยิ่งขึ้น
renderSequence : Array Step -> List (Html Msg) renderSequence sequence = Array.indexedMap renderStep sequence |> Array.toList
เราจับคู่แต่ละขั้นตอนในลำดับและส่งผ่านไปยังฟังก์ชัน renderStep
ในการแมป JavaScript กับดัชนีจะเขียนดังนี้:
sequence.map((node, index) => renderStep(index, node))
เมื่อเทียบกับ JavaScript การแมปใน Elm เกือบจะกลับกัน เราเรียก Array.indexedMap
ซึ่งรับสองอาร์กิวเมนต์: ฟังก์ชันที่จะใช้ในแผนที่ ( renderStep
) และอาร์เรย์ที่จะจับคู่ ( sequence
) renderStep
เป็นฟังก์ชันสุดท้ายของเราและเป็นตัวกำหนดว่าปุ่มทำงานอยู่หรือไม่ใช้งาน เราใช้ indexedMap
เนื่องจากเราจำเป็นต้องส่งต่อดัชนีขั้นตอน (ซึ่งเราใช้เป็น ID) ไปยังขั้นตอนนั้นๆ เพื่อส่งต่อไปยังฟังก์ชันอัปเดต
renderStep : Int -> Step -> Html Msg renderStep index step = let classes = if step == On then "step _active" else "step" in button [ class classes ] []
renderStep
ยอมรับดัชนีเป็นอาร์กิวเมนต์แรก ขั้นตอนเป็นอาร์กิวเมนต์ที่สอง และส่งคืน HTML ที่แสดงผล การใช้บล็อก let...in
เพื่อกำหนดฟังก์ชันท้องถิ่น เรากำหนดคลาส _active
ให้กับ On Steps และเรียกใช้ฟังก์ชันคลาสของเราในรายการแอตทริบิวต์ปุ่ม
กำลังอัปเดตสถานะแอปพลิเคชัน
ณ จุดนี้ แอพของเราแสดง 16 ขั้นตอนในลำดับการเตะ แต่การคลิกไม่ได้เปิดใช้งานขั้นตอน ในการอัปเดตสถานะขั้นตอน เราต้องส่งข้อความ ( Msg
) ไปยังฟังก์ชันอัปเดต เราทำสิ่งนี้โดยกำหนดข้อความและแนบไปกับตัวจัดการเหตุการณ์สำหรับปุ่มของเรา
ใน Types.elm เราจำเป็นต้องกำหนดข้อความแรกของเรา ToggleStep
จะใช้ Int
สำหรับดัชนีลำดับและ Step
ถัดไป ใน renderStep
เราแนบข้อความ ToggleStep
กับเหตุการณ์เมื่อคลิกของปุ่ม พร้อมด้วยดัชนีลำดับและขั้นตอนเป็นอาร์กิวเมนต์ การดำเนินการนี้จะส่งข้อความไปยังฟังก์ชันการอัปเดตของเรา แต่ ณ จุดนี้การอัปเดตจะไม่ทำอะไรเลย
type Msg = ToggleStep Int Step renderStep index step = let ... in button [ onClick (ToggleStep index step) , class classes ] []
ข้อความเป็นประเภทปกติ แต่เรากำหนดให้ เป็น ประเภทที่ทำให้เกิดการอัปเดต ซึ่งเป็นแบบแผนใน Elm ใน Update.elm เราปฏิบัติตาม Elm Architecture เพื่อจัดการกับการเปลี่ยนแปลงสถานะของโมเดล ฟังก์ชันอัปเดตของเราจะรับข้อความ Msg
และ Model
ปัจจุบัน และส่งคืนโมเดลใหม่และอาจได้รับคำสั่ง คำสั่งจัดการผลข้างเคียง ซึ่งเราจะพิจารณาในส่วนที่สอง เรารู้ว่าเราจะมีข้อความ Msg
หลายประเภท ดังนั้นเราจึงตั้งค่าบล็อกตัวพิมพ์ที่ตรงกันกับรูปแบบ สิ่งนี้บังคับให้เราจัดการกับกรณีทั้งหมดของเราในขณะเดียวกันก็แยกกระแสของรัฐ และคอมไพเลอร์จะมั่นใจได้ว่าเราจะไม่พลาดทุกกรณีที่สามารถเปลี่ยนโมเดลของเราได้
การอัปเดตบันทึกใน Elm ทำได้แตกต่างไปจากการอัปเดตวัตถุใน JavaScript เราไม่สามารถเปลี่ยนฟิลด์ในเร็กคอร์ดโดยตรงเช่น record.field = *
เพราะเราไม่สามารถใช้ this
หรือ self
ได้ แต่ Elm มีตัวช่วยในตัว ได้รับบันทึกเช่น brian = { name = "brian" }
เราสามารถอัปเดตฟิลด์ชื่อเช่น { brian | name = "BRIAN" }
{ brian | name = "BRIAN" }
. รูปแบบดังต่อไปนี้ { record | field = newValue }
{ record | field = newValue }
.
นี่คือวิธีการอัปเดตฟิลด์ระดับบนสุด แต่ฟิลด์ที่ซ้อนกันนั้นซับซ้อนกว่าใน Elm เราจำเป็นต้องกำหนดฟังก์ชันตัวช่วยของเราเอง ดังนั้นเราจะกำหนดฟังก์ชันตัวช่วยสี่ฟังก์ชันเพื่อเจาะลึกระเบียนที่ซ้อนกัน:
- หนึ่งเพื่อสลับค่าขั้นตอน
- หนึ่งเพื่อส่งคืนลำดับใหม่ มีค่าขั้นตอนที่อัพเดต
- อื่นเพื่อเลือกว่าลำดับใดของลำดับที่เป็นของ
- และฟังก์ชันสุดท้ายเพื่อส่งคืนแทร็กใหม่ซึ่งมีลำดับที่อัปเดตซึ่งประกอบด้วยค่าขั้นตอนที่อัปเดต
เราเริ่มต้นด้วย ToggleStep
เพื่อสลับค่าขั้นตอนของลำดับแทร็กระหว่างเปิดและปิด เราใช้ let...in
block อีกครั้งเพื่อสร้างฟังก์ชันที่เล็กลงภายในคำสั่ง case หากขั้นตอนนั้นปิดอยู่ เราก็จะทำให้เป็นเปิด และในทางกลับกัน
toggleStep = if step == Off then On else Off
toggleStep
จะถูกเรียกจาก newSequence
ข้อมูลไม่สามารถเปลี่ยนแปลงได้ในภาษาที่ใช้งานได้ ดังนั้น แทนที่จะ แก้ไข ลำดับ เรากำลังสร้างลำดับใหม่ด้วยค่าขั้นตอนที่อัปเดตเพื่อแทนที่ลำดับเดิม
newSequence = Array.set index toggleStep selectedTrack.sequence
newSequence
ใช้ Array.set
เพื่อค้นหาดัชนีที่เราต้องการสลับ จากนั้นจึงสร้างลำดับใหม่ หาก set ไม่พบดัชนี ก็จะส่งกลับลำดับเดียวกัน มันอาศัย selectedTrack.sequence
เพื่อทราบว่าจะแก้ไขลำดับใด selectedTrack
เป็นฟังก์ชันตัวช่วยหลักที่ใช้เพื่อให้เราเข้าถึงระเบียนที่ซ้อนกันของเราได้ ณ จุดนี้ มันง่ายอย่างน่าประหลาดใจเพราะโมเดลของเรามีแทร็กเดียวเท่านั้น
selectedTrack = model.track
ฟังก์ชันตัวช่วยสุดท้ายของเราเชื่อมต่อส่วนที่เหลือทั้งหมด อีกครั้ง เนื่องจากข้อมูลไม่สามารถเปลี่ยนแปลงได้ เราจึงแทนที่แทร็กทั้งหมดด้วยแทร็กใหม่ที่มีลำดับใหม่
newTrack = { selectedTrack | sequence = newSequence }
newTrack
ถูกเรียกนอก let...in
block ซึ่งเราส่งคืนโมเดลใหม่ ที่มีแทร็กใหม่ ซึ่งจะแสดงผลมุมมองใหม่ เราไม่ส่งผลข้างเคียง ดังนั้นเราจึงใช้ Cmd.none
อีกครั้ง ฟังก์ชัน update
ทั้งหมดของเรามีลักษณะดังนี้:
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of ToggleStep index step -> let selectedTrack = model.track newTrack = { selectedTrack | sequence = newSequence } toggleStep = if step == Off then On else Off newSequence = Array.set index toggleStep selectedTrack.sequence in ( { model | track = newTrack } , Cmd.none )
เมื่อเราเรียกใช้โปรแกรม เราจะเห็นแทร็กที่แสดงผลพร้อมชุดขั้นตอน การคลิกที่ปุ่มขั้นตอนใด ๆ จะเรียก ToggleStep
ซึ่งจะเข้าสู่ฟังก์ชันการอัปเดตของเราเพื่อแทนที่สถานะของโมเดล
เมื่อแอปพลิเคชันของเรามีขนาดใหญ่ขึ้น เราจะเห็นว่ารูปแบบที่ทำซ้ำได้ของ Elm Architecture ทำให้สถานะการจัดการง่ายขึ้นได้อย่างไร ความคุ้นเคยในฟังก์ชันโมเดล อัปเดต และมุมมองช่วยให้เรามุ่งความสนใจไปที่โดเมนธุรกิจของเรา และทำให้ง่ายต่อการข้ามไปยังแอปพลิเคชัน Elm ของบุคคลอื่น
หยุดพัก
การเขียนในภาษาใหม่ต้องใช้เวลาและการฝึกฝน โปรเจ็กต์แรกที่ฉันทำคือโคลน TypeForm ง่ายๆ ที่ฉันเคยเรียนรู้ไวยากรณ์ Elm สถาปัตยกรรม และกระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชัน ณ จุดนี้ คุณได้เรียนรู้พอที่จะทำสิ่งที่คล้ายกันแล้ว หากคุณกระตือรือร้น ขอแนะนำให้อ่านคู่มือการเริ่มต้นใช้งานอย่างเป็นทางการ Evan ผู้สร้างของ Elm จะแนะนำคุณเกี่ยวกับแรงจูงใจสำหรับ Elm, ไวยากรณ์, ประเภท, สถาปัตยกรรมของ Elm, การปรับขนาด และอื่นๆ โดยใช้ตัวอย่างที่ใช้งานได้จริง
ในส่วนที่สอง เราจะเจาะลึกถึงคุณสมบัติที่ดีที่สุดของ Elm: การใช้คอมไพเลอร์เพื่อปรับโครงสร้างลำดับขั้นตอนของเราใหม่ นอกจากนี้ เราจะได้เรียนรู้วิธีจัดการกับเหตุการณ์ที่เกิดซ้ำ การใช้คำสั่งสำหรับผลข้างเคียง และการโต้ตอบกับ JavaScript คอยติดตาม!