ภาษาการเขียนโปรแกรมกำหนดฟังก์ชั่นอย่างไร


28

ภาษาการเขียนโปรแกรมกำหนดและบันทึกฟังก์ชัน / วิธีอย่างไร ฉันกำลังสร้างภาษาการเขียนโปรแกรมตีความใน Ruby และฉันพยายามคิดวิธีการประกาศใช้ฟังก์ชั่น

แนวคิดแรกของฉันคือการบันทึกเนื้อหาของการประกาศในแผนที่ ตัวอย่างเช่นถ้าฉันทำสิ่งที่ชอบ

def a() {
    callSomething();
    x += 5;
}

จากนั้นฉันจะเพิ่มรายการลงในแผนที่ของฉัน:

{
    'a' => 'callSomething(); x += 5;'
}

ปัญหานี้คือว่ามันจะกลายเป็นแบบเรียกซ้ำเพราะฉันจะต้องเรียกparseวิธีการของฉันบนสตริงซึ่งจะโทรparseอีกครั้งเมื่อพบdoSomethingและจากนั้นฉันก็จะหมดพื้นที่สแต็คในที่สุด

ดังนั้นภาษาที่ตีความจะจัดการกับสิ่งนี้ได้อย่างไร


โอ้และนี่คือโพสต์แรกของฉันใน Programmers.SE ดังนั้นโปรดแจ้งฉันหากฉันทำอะไรผิดหรือเป็นนอกหัวข้อ :)
Doorknob

ในอดีตที่ผ่านมาฉันได้เก็บอินไลน์ทั้งหมดไว้ในโทเค็นและการเรียกใช้ฟังก์ชันต่าง ๆ เพิ่งข้ามไปยังออฟเซ็ตเฉพาะ (เหมือนป้ายในแอสเซมบลี) คุณโทเค็นสคริปต์หรือไม่ หรือแยกสตริงแต่ละครั้ง
Simon Whitehead

@SimonWhitehead ฉันแยกสตริงเป็นโทเค็นแล้วแยกแต่ละโทเค็นแยกกัน
Doorknob

3
หากคุณยังใหม่ต่อการออกแบบและการใช้งานโปรแกรมภาษาคุณอาจต้องการตรวจสอบวรรณกรรมบางเรื่องในหัวข้อ สิ่งที่ได้รับความนิยมมากที่สุดคือ "Dragon Book": en.wikipedia.org/wiki/…แต่มีข้อความที่กระชับกว่าและดีกว่าเช่นกัน ยกตัวอย่างเช่นการใช้ภาษาการเขียนโปรแกรมโดยอาร์เนแรานตาสามารถรับได้ฟรีที่นี่: bit.ly/15CF6gC
evilcandybag

1
@ddyer ขอบคุณ! ฉันไปหาล่ามเสียงกระเพื่อมในภาษาต่าง ๆ และนั่นช่วยได้จริงๆ :)
Doorknob

คำตอบ:


31

ฉันจะถูกต้องหรือไม่ถ้าสมมติว่าฟังก์ชั่น "วิเคราะห์คำ" ของคุณไม่เพียง แต่แยกวิเคราะห์โค้ด แต่ยังรันในเวลาเดียวกันได้หรือไม่ หากคุณต้องการทำเช่นนั้นแทนที่จะเก็บเนื้อหาของฟังก์ชั่นในแผนที่ของคุณให้จัดเก็บตำแหน่งของฟังก์ชั่น

แต่มีวิธีที่ดีกว่า ใช้ความพยายามเพิ่มขึ้นเล็กน้อย แต่ก็ให้ผลลัพธ์ที่ดีกว่ามากเมื่อความซับซ้อนเพิ่มขึ้น: ใช้แผนผังต้นไม้บทคัดย่อ

แนวคิดพื้นฐานคือคุณแยกวิเคราะห์รหัสเพียงครั้งเดียว จากนั้นคุณมีชุดของชนิดข้อมูลที่แสดงถึงการดำเนินงานและค่าต่างๆและคุณสร้างแผนภูมิของพวกเขาเช่น:

def a() {
    callSomething();
    x += 5;
}

กลายเป็น:

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(นี่เป็นเพียงการแสดงข้อความของโครงสร้างของ AST สมมุติต้นไม้จริงอาจจะไม่ได้อยู่ในรูปแบบข้อความ) อย่างไรก็ตามคุณแยกรหัสของคุณออกเป็น AST แล้วคุณเรียกใช้ล่ามของคุณผ่าน AST โดยตรง หรือใช้รหัสผ่านที่สอง ("การสร้างรหัส") เพื่อเปลี่ยน AST เป็นรูปแบบเอาต์พุตบางส่วน

ในกรณีของภาษาของคุณสิ่งที่คุณอาจจะทำคือมีแผนที่ที่แมปชื่อฟังก์ชั่นกับฟังก์ชั่น AST แทนชื่อฟังก์ชั่นกับฟังก์ชั่นสตริง


โอเค แต่ปัญหายังคงอยู่ที่นั่น: มันใช้การสอบถามซ้ำ ฉันจะใช้พื้นที่สแต็กในที่สุดถ้าฉันทำเช่นนี้
Doorknob

3
@ Doorknob: ใช้การเรียกซ้ำโดยเฉพาะคืออะไร ภาษาการเขียนโปรแกรมแบบโครงสร้างบล็อกใด ๆ (ซึ่งเป็นภาษาที่ทันสมัยทุกภาษาในระดับที่สูงกว่า ASM) นั้นมีพื้นฐานมาจากต้นไม้ คุณกังวลเรื่องใดเกี่ยวกับการทำให้สแตกล้น
Mason Wheeler

1
@Doorknob: ใช่นั่นเป็นคุณสมบัติโดยธรรมชาติของภาษาใด ๆ แม้ว่าจะถูกคอมไพล์ลงในรหัสเครื่อง (สแตกการเรียกเป็นการแสดงให้เห็นถึงพฤติกรรมนี้) จริง ๆ แล้วฉันเป็นผู้มีส่วนร่วมในระบบสคริปต์ที่ทำงานในวิธีที่ฉันอธิบาย เข้าร่วมแชทกับฉันที่chat.stackexchange.com/rooms/10470/และฉันจะพูดถึงเทคนิคบางอย่างสำหรับการตีความที่มีประสิทธิภาพและลดผลกระทบต่อขนาดสแต็คกับคุณ :)
Mason Wheeler

2
@Doorknob: มีปัญหาการเรียกซ้ำไม่ได้ที่นี่เพราะการเรียกฟังก์ชั่นใน AST มีการอ้างอิงถึงฟังก์ชั่นโดยชื่อก็ไม่จำเป็นต้องมีการอ้างอิงถึงที่เกิดขึ้นจริงฟังก์ชั่น หากคุณกำลังรวบรวมรหัสเครื่องในที่สุดคุณจะต้องใช้ที่อยู่ฟังก์ชันซึ่งเป็นสาเหตุที่ผู้รวบรวมส่วนใหญ่ทำการส่งหลายครั้ง หากคุณต้องการคอมไพเลอร์แบบ One-Passคุณต้องมี "การประกาศไปข้างหน้า" ของฟังก์ชั่นทั้งหมดเพื่อให้คอมไพเลอร์สามารถกำหนดที่อยู่ไว้ล่วงหน้า คอมไพเลอร์ Bytecode ไม่ได้สนใจสิ่งนี้ตัวจัดการการค้นหาชื่อ
Aaronaught

5
@ Doorknob: มันซ้ำได้แน่นอน และใช่หากสแต็กของคุณมีเพียง 16 รายการคุณจะไม่สามารถแยกวิเคราะห์(((((((((((((((( x )))))))))))))))))ได้ ในความเป็นจริงกองสามารถมีขนาดใหญ่กว่าและความซับซ้อนทางไวยากรณ์ของรหัสจริงค่อนข้าง จำกัด แน่นอนว่ารหัสนั้นจะต้องอ่านได้โดยมนุษย์
MSalters

4

คุณไม่ควรเรียกการแยกวิเคราะห์เมื่อเห็นcallSomething()(ฉันเข้าใจว่าคุณหมายถึงcallSomethingมากกว่าdoSomething) ความแตกต่างระหว่างaและcallSomethingเป็นที่หนึ่งคือการกำหนดวิธีการในขณะที่อื่น ๆ คือการเรียกวิธีการ

เมื่อคุณเห็นคำนิยามใหม่คุณจะต้องทำการตรวจสอบที่เกี่ยวข้องเพื่อให้แน่ใจว่าคุณสามารถเพิ่มคำจำกัดความดังกล่าวได้ดังนั้น:

  • ตรวจสอบว่าไม่มีฟังก์ชั่นที่มีลายเซ็นเดียวกัน
  • ตรวจสอบให้แน่ใจว่ามีการประกาศวิธีการในขอบเขตที่เหมาะสม (เช่นสามารถประกาศวิธีการภายในการประกาศวิธีการอื่นได้หรือไม่)

ถ้าผ่านการตรวจสอบเหล่านี้คุณสามารถเพิ่มลงในแผนที่ของคุณและเริ่มตรวจสอบเนื้อหาของวิธีการนั้น

เมื่อคุณพบวิธีการโทรcallSomething()คุณควรทำการตรวจสอบต่อไปนี้:

  • ไม่callSomethingอยู่ในแผนที่ของคุณหรือไม่
  • มันถูกเรียกอย่างถูกต้อง (จำนวนข้อโต้แย้งตรงกับลายเซ็นที่คุณพบ)?
  • ข้อโต้แย้งนั้นใช้ได้หรือไม่ (หากใช้ชื่อตัวแปรพวกเขาจะประกาศหรือไม่สามารถเข้าถึงได้ในขอบเขตนี้หรือไม่)
  • สามารถโทรหาบางสิ่งจากที่ที่คุณอยู่ (เป็นส่วนตัวสาธารณะได้รับการคุ้มครองหรือไม่)?

หากคุณพบว่าcallSomething()ไม่เป็นไร ณ จุดนี้สิ่งที่คุณต้องการจะทำจริง ๆ ขึ้นอยู่กับว่าคุณต้องการที่จะเข้าใกล้มันอย่างไร เมื่อพูดอย่างเคร่งครัดเมื่อคุณรู้ว่าการโทรดังกล่าวไม่เป็นไรในตอนนี้คุณสามารถบันทึกชื่อของเมธอดและอาร์กิวเมนต์ได้โดยไม่ต้องลงรายละเอียดเพิ่มเติม เมื่อคุณเรียกใช้โปรแกรมของคุณคุณจะเรียกใช้เมธอดด้วยอาร์กิวเมนต์ที่คุณควรมีในขณะทำงาน

ถ้าคุณต้องการที่จะไปต่อคุณสามารถบันทึกไม่เพียง แต่สตริง แต่ลิงค์ไปยังวิธีการที่เกิดขึ้นจริง นี่จะมีประสิทธิภาพมากกว่า แต่ถ้าคุณต้องจัดการหน่วยความจำมันอาจทำให้สับสนได้ ฉันอยากจะแนะนำให้คุณยึดสายไว้ในตอนแรก หลังจากนั้นคุณสามารถลองปรับให้เหมาะสม

โปรดทราบว่าทั้งหมดนี้ถือว่าคุณได้เพิ่มโปรแกรมของคุณซึ่งหมายความว่าคุณได้รับรู้โทเค็นทั้งหมดในโปรแกรมของคุณและรู้ว่าพวกเขาคืออะไร นั่นไม่ได้เป็นการบอกว่าคุณรู้ว่าพวกเขาเข้ากันได้ดีหรือไม่ซึ่งเป็นขั้นตอนการแยกวิเคราะห์ หากคุณยังไม่ทราบว่าโทเค็นคืออะไรฉันขอแนะนำให้คุณมุ่งเน้นไปที่การรับข้อมูลก่อนเป็นอันดับแรก

ฉันหวังว่าจะช่วย! ยินดีต้อนรับสู่โปรแกรมเมอร์ SE!


2

อ่านโพสต์ของคุณฉันสังเกตเห็นสองคำถามในคำถามของคุณ สิ่งที่สำคัญที่สุดคือวิธีการแยกวิเคราะห์ มีตัวแยกวิเคราะห์หลายชนิด (เช่นตัวแยกวิเคราะห์แบบวนซ้ำ Recursive , LR Parsers , Packrat Parsers ) และตัวแยกวิเคราะห์ (เช่นGNU bison , ANTLR ) ที่คุณสามารถใช้เพื่อท่องไปตามไวยากรณ์ของโปรแกรม

คำถามที่สองเป็นเรื่องเกี่ยวกับรูปแบบการจัดเก็บข้อมูลสำหรับฟังก์ชั่น เมื่อคุณไม่ได้ทำการแปลกำกับด้วยไวยากรณ์คุณจะสร้างการแสดงสื่อกลางของโปรแกรมของคุณซึ่งอาจเป็นทรีของไวยากรณ์ที่เป็นนามธรรมหรือภาษากลางที่กำหนดเองบางอย่างเพื่อทำการประมวลผลเพิ่มเติมด้วย (คอมไพล์เปลี่ยนรูปดำเนินการเขียน ไฟล์ ฯลฯ )


1

จากมุมมองทั่วไปความหมายของฟังก์ชั่นนั้นเล็กกว่าฉลากหรือบุ๊คมาร์คเล็กน้อยในรหัส ขอบเขตลูปและโอเปอเรเตอร์เงื่อนไขอื่น ๆ ส่วนใหญ่จะคล้ายกัน พวกเขาเป็นแบบสแตนด์อะโลนสำหรับคำสั่ง "ข้าม" หรือ "ข้ามไป" ในระดับที่ต่ำกว่าของนามธรรม การเรียกใช้ฟังก์ชั่นนั้นจะลดระดับลงไปที่คำสั่งคอมพิวเตอร์ระดับล่างต่อไปนี้:

  • เชื่อมต่อข้อมูลของพารามิเตอร์ทั้งหมดรวมทั้งตัวชี้ไปยังคำสั่งถัดไปของฟังก์ชันปัจจุบันลงในโครงสร้างที่เรียกว่า "call stack frame"
  • กดเฟรมนี้ลงบนสแต็กการโทร
  • ข้ามไปยังหน่วยความจำออฟเซ็ตของบรรทัดแรกของรหัสฟังก์ชัน

คำสั่ง "return" หรือคล้ายกันจะทำสิ่งต่อไปนี้:

  • โหลดค่าที่จะส่งคืนลงทะเบียน
  • โหลดตัวชี้ไปยังผู้โทรเข้าสู่การลงทะเบียน
  • วางเฟรมสแต็กปัจจุบัน
  • ข้ามไปที่ตัวชี้ของผู้โทร

ดังนั้นฟังก์ชั่นจึงเป็นเพียงนามธรรมในสเปคภาษาระดับสูงขึ้นซึ่งช่วยให้มนุษย์สามารถจัดระเบียบโค้ดได้ในลักษณะที่บำรุงรักษาและใช้งานง่ายขึ้น เมื่อรวบรวมเป็นภาษาแอสเซมบลีหรือภาษากลาง (JIL, MSIL, ILX) และแน่นอนเมื่อแสดงผลเป็นรหัสเครื่อง abstractions เกือบทั้งหมดจะหายไป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.