การเรียนรู้เอล์มจากตัวจัดลำดับกลอง (ตอนที่ 1)

เผยแพร่แล้ว: 2022-03-10
บทสรุปโดยย่อ ↬ ผู้พัฒนา Front-end Brian Holt จะแนะนำผู้อ่านตลอดการสร้างตัวจัดลำดับกลองใน Elm ในส่วนที่หนึ่งของซีรีส์สองส่วนนี้ เขาได้แนะนำไวยากรณ์ การตั้งค่า และแนวคิดหลักของ Elm คุณจะได้เรียนรู้วิธีทำงานกับสถาปัตยกรรม Elm เพื่อสร้างแอปพลิเคชันอย่างง่าย

หากคุณเป็นนักพัฒนาส่วนหน้าตามวิวัฒนาการของแอปพลิเคชันหน้าเดียว (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

แอพ Elm แสดงตัวเลขที่เพิ่ม
แอพ Bare-bones Elm ของเราที่แสดงตัวเลขที่เพิ่มบนหน้าจอ

การสร้างแบบจำลองข้อมูลด้วยประเภท

มาจากภาษาที่พิมพ์แบบไดนามิกเช่น 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>

เราจะสร้างสามฟังก์ชันเพื่อแสดงมุมมองของเรา:

  1. หนึ่งรายการเพื่อแสดงแทร็กเดียว ซึ่งประกอบด้วยชื่อแทร็กและซีเควนซ์
  2. อื่นเพื่อแสดงลำดับตัวเอง
  3. และอีกอันหนึ่งเพื่อแสดงปุ่มแต่ละขั้นตอนภายในลำดับ

ฟังก์ชันมุมมองแรกของเราจะแสดงแทร็กเดียว เราใช้คำอธิบายประกอบประเภทของเรา 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 เราจำเป็นต้องกำหนดฟังก์ชันตัวช่วยของเราเอง ดังนั้นเราจะกำหนดฟังก์ชันตัวช่วยสี่ฟังก์ชันเพื่อเจาะลึกระเบียนที่ซ้อนกัน:

  1. หนึ่งเพื่อสลับค่าขั้นตอน
  2. หนึ่งเพื่อส่งคืนลำดับใหม่ มีค่าขั้นตอนที่อัพเดต
  3. อื่นเพื่อเลือกว่าลำดับใดของลำดับที่เป็นของ
  4. และฟังก์ชันสุดท้ายเพื่อส่งคืนแทร็กใหม่ซึ่งมีลำดับที่อัปเดตซึ่งประกอบด้วยค่าขั้นตอนที่อัปเดต

เราเริ่มต้นด้วย 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 คอยติดตาม!