Skip to content

RevensofT/FVB

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 

Repository files navigation

Functional Visual Basic

Let's embark to functional way !!


ตอนนี้เรามีอะไรกันบ้าง

  • Recursion
  • Pipeliner
  • Link list
  • Tail call loop
  • Immutable

Object ต่างๆ ที่เกี่ยวข้องกับการเขียนแบบ functional ที่ต้องใช้ประกอบ

Value tuple

Value tuple นั้นเริ่มมีใช้ใน .net framework 4.7 เป็นต้นไป(และ Visual Basic 15.3 สำหรับการเรียกใช้ชื่อสมาชิกซ้ำซ้อน) Object นี้เป็นส่วนสำคัญมากเนื่องจากเป็นการสร้าง structure แบบอัตโนมัติซึ่งช่วยให้เราใช้ value type ได้อย่างสะดวกและหลากหลาย

  • วิธีสร้างแบบไม่ตั้งชื่อสมาชิก
Dim Data = (999, 666)
'Data.Item1 == 999
'Data.Item2 == 666
Dim Data As (Integer, Integer) = (999, 666)
'Data.Item1 == 999
'Data.Item2 == 666
  • การสร้างแบบตั้งชื่อสมาชิก
Dim Data = (a:=999, b:=666)
'Data.a == 999
'Data.b == 666
Dim Data As (a As Integer, b As Integer) = (999, 666)
'Data.a == 999
'Data.b == 666

Lamda method

Lamda method เป็นการสร้างฟังค์ชั่นขึ้นมาภายในฟังค์ชั่นนั้นๆ ซึ่งจะถูกเก็บเอาไว้ในรูปแบบ fuction pointer หรือ Delegate ทำให้เราไม่ต้องวิ่งไปวิ่งมาเที่ยวสร้างฟังค์ชั่นไว้ที่นู้นนี่นั้นให้ต้องมานั่งหาเวลากลับมาอ่านโค๊ดแถมยังจำกัดขอบเขตการใช้งานให้ฟังค์ชั่นนั้นๆ สามารถใช้งานเฉพาะในฟังค์ชั่นที่ถูกสร้างได้อีกด้วย

  • การสร้าง Lamda method แบบปรกติ
Dim Plus1 = Function(Input As Integer) As Integer
                Return Input + 1
            End Function

Dim Write = Sub(Input As Integer)
                Console.Write(Input)
            End Sub
  • การสร้าง Lamda method แบบลดรูป ซึ่งการสร้างแบบนี้นั้นจะจำกัดโค๊ดในฟังค์ชั่นนั้นๆ ให้ยาวเพียง 1 การทำการ(ไม่จำเป็นต้อง 1 บรรทัด)
Dim Plus1 = Function(Input As Integer) Input + 1

Dim Write = Sub(Input As Integer) Console.Write(Input)
  • การสร้าง Lamda method แบบใช้ Infer หรือการคาดการณ์ type ของวัตถุ การใช้สร้างแบบนี้นั้นจะต้องมีองค์ประกอบที่ทำให้ Intellisense นั้นคาดการณ์ได้ว่าฟังค์ชั่นนั้นๆ จะรับค่าแบบไหนเข้าไปบ้างและคืนค่าอะไรกลับออกมา
Dim Plus1 As Func(Of Integer, Integer) = Function(Input) Input + 1

Dim Write As Action(Of Integer) = Sub(Input) Console.Write(Input)

ข้อควรระวังสำคัญในการใช้ lamda method นั้น ต้องระวังการนำ value และ varient จากภายนอกเข้ามาใช้ในฟังค์ชั่นเนื่องจากคอมไพล์เลอร์จะสร้างคลาสลับขึ้นมาเพื่อผนวกค่านั้นๆ เข้ากับ lamda method ซึ่งจะเป็นการเพิ่มงาน GC โดยใช่เหตุ

Pipeline

Pipeline เป็นแนวการเขียนแบบทำหลายๆ อย่างใน 1 การทำการ

Call (Sub(Input As Integer) Console.Write(Input))(Function(Input As Integer) Input + 1)(8))
' Console write :: 9

ฟังค์ชั่นต่างๆ ใน FVB รุ่นล่าสุด

Recursion

การเรียกใช้ฟังค์ชั่นนั้นๆ ซ้ำๆ เองนั้นโดยปรกติแล้วจะทำให้เกิดการดอง stack เกิดขึ้นซึ่งจะนำมาซึ่งปัญหา Stack overflowได้ ดังนี้เราจึงจำเป็นต้องมีฟังค์ชั่นที่จะช่วยทำการ tail call/jmp เพื่อไม่ให้เกิดปัญหาดังกล่าว

<Extension>
recursive(Of T)(Data As T, Do_until_condition As Func(Of T, Boolean), Recursive_method As Func(Of T, T)) As T
  • Data ข้อมูลที่จำเป็นที่เราต้องใช้เพื่อให้ได้ผลลัพย์ที่ต้องการ
  • Do_until_condition ฟังค์ชั่นตรวจสอบเงื่อนไขที่จะบอกฟังค์ชั่น recursive ว่าเราไม่ต้องการเรียกใช้ Recursive_method ซ้ำอีกต่อไปแล้ว
  • Recursive_method ฟังค์ชั่นที่เราต้องการจะเรียกใช้ซ้ำๆ

ตัวอย่างการใช้งาน

Console.WriteLine(
    (input:=5, sum:=1).recursive(
        Function(Data As (input As Integer, sum As Integer))  Data.input < 2,
        Function(Data As (input As Integer, sum As Integer)) (Data.input - 1, Data.sum * Data.input)
    ).sum
)
' Console write :: 120
Console.WriteLine(
    (input:=5, sum:=1).recursive(
        Function(Data)  Data.input < 2,
        Function(Data) (Data.input - 1, Data.sum * Data.input)
    ).sum
)
' Console write :: 120
<Extension>
recur(Of T)(Data As (Is_end As Boolean, o As T), Recursive_method As Func(Of (Is_end As Boolean, o As T), (Is_end As Boolean, o As T))) As T
  • Data
    • Is_end หากมีค่าเป็น True ให้ทำการหยุดฟังค์ชั่น
    • o ข้อมูลที่จำเป็นที่เราต้องใช้เพื่อให้ได้ผลลัพย์ที่ต้องการ
  • Recursive_method ฟังค์ชั่นที่เราต้องการจะเรียกใช้ซ้ำๆ
Console.WriteLine(
    (input:=5, sum:=1).recur(
        Function(G) (G.input < 2, (G.input - 1, G.sum * G.input))
    ).sum
)
' Console write :: 120

Pipeliner

เป็น method ช่วยให้สามารถทำการอื่นๆ กับ ค่านั้นๆ ได้ในระหว่าง 1 การทำการนั้นๆ ซึ่งตัว Pipeliner จะคืนค่าที่รับเข้ามาเสมอและค่าที่ส่งไปยัง Method เองนั้นก็เป็นค่าที่รับเข้ามาเช่นกัน

<Extension>
call(Of T)(Data As T, Method As Action(Of T)) As T
Dim Word = "Hello".call(Sub(G)Console.Write(G))
' Word = "Hello"
' Console write :: Hello
<Extension>
call(Of T, V)(Data As T, Param As V, Method As Action(Of T, V)) As T
Dim Word = "Hello".call(" world.", Sub(G, P)Console.Write(G & P))
' Word = "Hello"
' Console write :: Hello world.

Link list

เป็นการสร้างลิงค์ลิซด้วย Value tuple

<Extension>
link(Of T As Structure, V)(O As T) As (l As int, o As T)
<Extension>
put(Of T As Structure, V)(List As (int, T), Val As V) As (l As int, o As (V, T))
<Extension>
list(Of T, V As Structure)(Input As V, _Index As Integer) As T
  • link เพิ่มค่าตัวเลขเอาไว้หน้าสุดเพื่อระบุว่าลิงค์นี้มีสมาชิกสุดท้ายลำดับที่เท่าไหร
  • put LIFO หุ้มค่าใหม่ใส่ก่อนหน้าค่าเก่าเช่น (0,1).link.put(2).put(3) == (l:=3, o:=(3, (2, (0, 1))));
  • list เข้าถึงค่าแบบเดียวกับอาเรย์ด้วยการระบุตำแหน่งที่ แทนการเรียกชื่อสมาชิก
Dim Link_list = (" my", "Hello").link.put(" brand").put(" new").put(" world.")
Link_list.loop(Sub(G, I) Console.Write(G.list(Of String)(I)))
' Console write :: Hello my brand new world.
<Extension>
pop(Of T As Structure, V)(List As (int, (V, T))) As (val As V, list As (int, o As T))
Dim Link_list = (" my", "Hello").link(Of String).put(" brand").put(" new").put(" world.")
Link_list.pop.call(Sub(G) Console.WriteLine(G.val)).list.each(Of String, String)("_", Sub(Item, P) Console.Write(Item & P))

' Console write :: world.
' Console write :: Hello_ my_ brand_ new_

Tail call loop

การวนแบบทิ้งหางเดิม หรือ tail call/jmp นั้นแตกต่างจากการวนด้วย for do while แบบปรกติที่จะใช้คำสั่ง br/goto ในการทวนคำสั่งภายในฟังค์ชั่นนั้นๆ ซึ่งไม่ต่างจากการเขียนฟังค์ชั่นยาวๆ เลยสำหรับ CPU

ฟั้งค์ชั่นในกลุ่ม Tail call loop นี้จะไม่ทำการอัพเดตนำเข้าค่าเหมือนกับฟังค์ชั่นในกลุ่ม Recursion

  • Array loop
<Extension>
each(Of T)(Array As T(), For_each As Action(Of T)) As T()
Call {1, 2, 3, 4, 5}.each(Sub(Item) Console.Write(Item))
' Console write :: 54321
<Extension>
each(Of T, V)(Array As T(), Param As V, For_each As Action(Of T, V)) As (T(), V)
Call {1, 2, 3, 4, 5}.each(10, Sub(Item, P) Console.Write(P - Item))
' Console write :: 56789
<Extension>
loop(Of T)(Array As T(), Do_loop As Action(Of T(), Integer)) As T()
Call {1, 2, 3, 4, 5}.loop(Sub(Array, Index) Console.Write(Array(Index)))
' Console write :: 54321
<Extension>
loop(Of T, V)(Array As T(), Param As V, Do_loop As Action(Of T(), Integer, V)) As (T(), V)
Call {1, 2, 3, 4, 5}.loop(10, Sub(Array, Index, P) Console.Write(P - Array(Index)))
' Console write :: 56789
  • Link list loop
<Extension>
each(Of T As Structure, V)(List As (int, T), For_each As Action(Of V)) As T
Dim Link_list = (" my", "Hello").put(" brand").put(" new").put(" world.").link(4)
Link_list.each(Of String)(Sub(Item) Console.Write(Item))
' Console write :: Hello my brand new world.
<Extension>
each(Of T As Structure, R, V)(List As (int, T), Param As R, For_each As Action(Of V, R)) As (T, R)
Dim Link_list = (" my", "Hello").put(" brand").put(" new").put(" world.").link(4)
Link_list.each(Of String, String)("_", Sub(Item, P) Console.Write(Item & P))
' Console write :: Hello_ my_ brand_ new_ world._
<Extension>
loop(Of T As Structure)(List As (int, T), Do_loop As Action(Of T, Integer)) As T
Dim Link_list = (" my", "Hello").put(" brand").put(" new").put(" world.").link(4)
Link_list.loop(Sub(G, I) Console.Write(G.list(Of String)(I)))
' Console write :: Hello my brand new world.
<Extension>
loop(Of T As Structure, V)(List As (int, T), Param As V, Do_loop As Action(Of T, Integer, V)) As (T, V)
Dim Link_list = (" my", "Hello").put(" brand").put(" new").put(" world.").link(4)
Link_list.loop("_", Sub(G, I, P) Console.Write(G.list(Of String)(I) & P))
' Console write :: Hello_ my_ brand_ new_ world._
  • Do loop
<Extension>
loop(Of T)(Data As T, Max As Integer, Do_loop As Action(Of T, Integer)) As T
Call {1, 2, 3, 4, 5}.loop(2, Sub(Array, Index) Console.Write(Array(Index)))
' Console write :: 321
<Extension>
loop(Of T)(Data As T, Max As Integer, Min As Integer, Do_loop As Action(Of T, Integer)) As T
Call {1, 2, 3, 4, 5}.loop(3, 1, Sub(Array, Index) Console.Write(Array(Index)))
' Console write :: 234

Immutable

เนื่องจากการเขียนแบบ functional นี้จะใช้ value type เสียมากผมเลยเห็นว่าการที่สามารถกำหนดให้ค่านั้นๆ เปลี่ยนแปลงไม่ได้จะช่วยป้องกันเหตุที่คาดไม่ถึงได้

<Extension>
refer(Of T As Structure)(Input As T) As reference(Of T)
<Extension>
read_only(Of T As Structure)(Input As T) As constant(Of T)
<Extension>
seal(Of T As Structure)(Input As T) As immutable(Of T)
  • reference เป็นคลาสช่วยในการ boxing ค่า
  • constant เป็นการ boxing ค่าเช่นกันแต่ค่านี้จะเปลี่ยนแปลงไม่ได้
  • immutable เป็นการทำให้ค่าเปลี่ยนแปลงไม่ได้เฉยๆ ไม่ได้ทำการ boxing แต่อย่างใด

Guildline แนวทางในการเขียนสไตล์ functional

  • Stay fresh พยายามรักษาความสดใหม่ของค่านั้นๆ เอาไว้ หรืออีกนัยหนึ่งก็คือแม้ตัวแปรนั้นๆ จะไม่ได้เป็น immutable แต่เราก็จะถือว่ามันเป็นตัวแปรแบบแก้ไขค่าไม่ได้

  • Less variant ลดละเลิกการใช้ตัวแปรภายในฟังค์ชั่น หรืออย่างน้อยก็ใช้ Anonymous local variant ซึ่งใน VB นั้นจะมี With statementอยู่ อย่างไรก็ดีพยายามให้ไม่มีจะดีกว่า

  • No side effect ฟังค์ชั่นไหนฟังค์ชั่นนั้น มีหน้าที่อะไรก็ให้ทำไปตามนั้น ไม่ควรแทรกงานอื่นๆ เข้าไปในฟังค์ชั่น ถ้าจะทำอย่างอื่นก็ให้สร้างฟังค์ชั่นใหม่มารับหน้าที่นั้นๆ แทน

A pure function is a function where the return value is only determined by its input values, without observable side effects. This is how functions in math work: Math.cos(x) will, for the same value of x , always return the same result. Computing it does not change x.

  • Short and clear พยายามทำให้ฟังค์ชั่นนั้นๆ สั้นและชัดเจนที่สุดเท่าที่จะทำได้ ไม่อย่างนั้นอาจจะเกิดอาการยืนงงอยู่กลางดง pipeline ได้ง่ายๆ ครับ