#F למפתחי #C

6 ביולי 2014

תגיות: , ,
אין תגובות

#F (הוגים f-sharp בדיוק כמו c-sharp) היא עוד אחת משפות התכנות מבית מיקרוסופט לסביבת ה .NET, שמגיעה כחלק אינטגרלי כבר מגרסת Visual Studio 2010.
השפה פותחה ע"י Don Syme מ- Microsoft Research. השפה היא מסוג Static type כמו #C ובשונה למשל מ- JavaScript שמוגדרת כ- dynamic type.
הפרדיגמות שלה הם תכנות אימפרטיבי בדומה ל- C, תכנות מונחה עצמים בדומה ל- C++ ו- C# והעיקרית שבהם היא תכנות פונקציונלי ובה נתמקד בפוסט זה.

כיום יש ל- #F קהילה מאד רחבה שתורמת להתפתחות השפה, הכלים לפיתוח וחומרי לימוד. השפה והכלים הם פרויקט קוד פתוח וכיום ניתן גם לפתח ב- F# עם Xamarin לכל הסביבות (Android, iOS, Windows Pone) גם ב- Visual Studio וגם ב- Xamarin Studio בלינוקס או מק.

לינקים מעניינים ב F#:

למי שרוצה לקרוא עוד על השפה ושימושיה, מוזמן להכנס ללינקים הבאים:

http://visualfsharp.codeplex.com/
http://fsharpforfunandprofit.com/
http://www.tryfsharp.org/
http://tomasp.net/blog/

תוכנית Hello World מאד קצרה ב- #F

printfn "Hello, World!"

בסה"כ מפעילים את הפונקציה printfn שמדפיסה למסך את המחרוזת Hello, World! שמועברת כפרמטר.

פונקציה כמבנה בסיסי בשפה

בפוסט זה יוצגו כמה תכונות של תכנות פונקציונלי. התכונה הבסיסית ביותר היא שפונקציה היא המרכיב הבסיסי ושניתן להעביר פונקציה כפרמטר לפונקציה ולהחזיר פונקציה כתוצאה של פונקציה ולשמור פונקציות בתוך משתנים בדומה למה שמתאפשר בעזרת delegate ב- C# לדוגמא:

   1: let add1 x = x + 1 // int -> int

   2: let mul2 x = x * 2 // int -> int

   3: let compose (f: int->int) (g: int->int) = // (int -> int) -> (int -> int) -> (int -> int)

   4:     let composeFunc x = g (f x)

   5:     composeFunc

   6:  let composefunc = compose addOne mul2  // int -> int

   7:  composefunc 3  // int

  • בשורה (1) מוגדרת פונקציה add1 שהיא מקבלת int ומחזירה int והיא מוסיפה לו 1 הסימון בהערה על שם הפונקציה מסמל את הטיפוס של הפונקציה ניתן לראות שלמרות שלא כתוב במפורש הטיפוסים הקוד הוא תקין ב F# והקומפיילר יודע להוציא את הטיפוסים בעזרת מנגנון שנקרא Type Inference.
  • בשורה (2) מוגדרת פונקציה mul2 שהיא גם מקבלת int ומחזירה int והיא מכפילה ב 2.
  • בשורה (3) מוגדרת הפונקציה compose שהיא מקבלת 2 פונקציות שמקבלות int ומחזירות int ומחזירה פונקציה חדשה מאותו סוג (פונקציה כזאת נקראת בשפה המקצועית Higher Order Function) בעזרת הגדרה של פונקציה פנימית.
  • בשורה (6) מעבירים לפונקציה compose את שתי הפונקציות add1 ו 2mul ומקבלים פונקציה חדשה מאותו סוג שלוקחת מספר מוסיפה לו 1 ומכפילה ב 2 ומחזירה אותו לכן כשמריצים את התוכנית התוצאה של שורה (7) היא 8 כי (3+1)*2 =8.

במאמר מוסגר בתכנות פונקציונלי הפונקציה compose מאד חשובה ומאד שימושית וב F# יש לה אפילו אופרטור << .

התמרות לעומת מוטציות

תכונה נוספת בתכנות פונקציונלי היא שלא ניתן לשנות ערך קיים ובמקום לייצר ערך חדש עם השינויים הנדרשים(Immutability) ממש כמו מחרוזות ב C# כל פעם שמנסים לשנות מחרוזת נוצרת מחרוזת חדשה ושפונקציה לא יכולה לשנות ערכים של פונקציה אחרת (פונקציה לא יכולה לגרום ל Side Effects תכונה זו מאד עוזרת בתכנות מקבילי).

ובעזרת דוגמא לחישוב סכום הריבועים של רשימת מספרים בצורה פונקציונלית ובצורה אימפרטיבית ההבדל יהיה ברור:

   1: let square x = x * x // (1) int -> int

   2:  

   3: let imperativeSum (numbers : int list) = // (2) int list -> int

   4:     let mutable total = 0

   5:     for i in numbers do

   6:         let x = square i

   7:         total <- total + x

   8:     total

   9:  

  10: let imperativeSumOneToTen = imperativeSum [1..10] // (3) int

  11:  

  12: let functionalSum (numbers : int list) = // (4) int list -> int

  13:     numbers

  14:     |> List.map square

  15:     |> List.sum

  16:  

  17: let functionalSumOneToTen = functionalSum [1..10] // (5) int

  • בשורה (1) מוגדרת פונקציה שכפילה מספר בעצמו.
  • בשורה (2) מוגדרת הפונקציה imperativeSum שמחשבת את סכום הריבועים בצורה אימפרטיבית ע"י כך שמוגדר המשתנה total כמשתנה שניתן לשנות אותו בשונה ממשתנה רגיל ב F# עוברים על הרשימה ובכל איטרציה מחשבים את ריבוע המספר וסוכמים לתוך total ובסוף מחזירים את total.
  • בשורה (3) מפעילים את הפונקציה על רשימה שמכילה את המספרים מ 1 עד 10 ומקבלים 385.
  • בשורה (4) מוגדרת הפונקציה functionalSum שמחשבת את סכום הריבועים בצורה פונקציונלית ומאד דומה ל linq ב C# שהגיע מהקונספט של תכנות פונקציונלי ב C# בפונקציה נראת כך:
    public static int FunctionalSum(List<int> numbers)

    {

        numbers.Select(n => n*n).Sum();

    }

הפונקציות Select ו map עושות את אותה הפעולה.  האופרטור |> (Pipe) מפשט את כתיבת הקוד ובמקום לרשום 
let functionalSum (numbers : int list) = // (4) int list -> int

    List.sum (List.map square numbers)

 

מקבלים קוד שהו יותר זורם ונקרא כך לוקחים את הרשימה numbers לכל מספר ממפים מספר חדש שהוא תוצאה של הפונקציה square וסוכמים את המספרים.
בשורה (5) מפעילים את הפונקציה functionalSum וגם היא מחזירה את אותה התוצאה 385.

מבני נתונים ותבניות

שתי תכונות נוספות מאד חזקות בשפות פונקציונליות הן שמערכת הטיפוסים (Type System) מאד עשירה דוגמא אחת היא Algebric Data Type וב F# נקרא Discriminated Union זהו טיפוס אשר מגדיר רשימה של אפשרויות לבחירה ובעזרתו ניתן לבנות מבני נתונים מורכבים כמו רשימות מקושרות ועצים בעזרת הגדרות פשוטות ו Pattern Matching בעזרתו ניתן לבצע פעולות לפי תבניות מוגדרות על מבנה נתונים לדוגמה Discriminated Union. בעזרת דוגמא לחישוב ביטויים מתמטיים פשוטים יודגמו שתי התכונות:

type Op = // (1)      

    | Add

    | Sub

    | Mul

    | Div

 

type Token = // (2)    

    | Num of int

    | Op of Op

 

let calc (expression : string) = // (3) string -> int

                                 

    let charToToken c = // (4) char -> Token                   

        match c with

        | d when System.Char.IsDigit d -> Num(System.Int32.Parse(d.ToString()))

        | op when op = '+' -> Op(Add)

        | op when op = '-' -> Op(Sub)

        | op when op = '*' -> Op(Mul)

        | op when op = '/' -> Op(Div)

        | _ -> failwith "char is not digit or known operator"

    

    let rec groupNum tokens = // (5) Token list -> Token list                             

        match tokens with

        | Num(n1) :: Num(n2) :: r -> groupNum ([ Num(10 * n1 + n2) ] @ r)

        | Num(n) :: Op(op) :: rest -> 

            [ Num(n);

              Op(op) ] @ groupNum rest

        | l -> l

    

    let rec toPostfix (ops : Op list) tokens = // (6) Token list -> Token list                                                            

        let precedence op = 

            match op with

            | Add -> 0

            | Sub -> 0

            | Mul -> 1

            | Div -> 1

        

        let push (ops : Op list) op = 

            if ops = [] || precedence op > precedence ops.Head then ([], [ op ] @ ops)

            else ([ Op(ops.Head) ], [ op ] @ ops.Tail)

        

        match tokens with

        | Num(n) :: rest -> Num(n) :: (toPostfix ops rest)

        | Op(op) :: rest -> 

            let op, ops = push ops op

            op @ (toPostfix ops rest)

        | [] -> ops |> List.map (fun o -> Op(o))

    

    let rec eval (num : int list) tokens = // (7) Token list -> int list                               

        let push (num : int list) n = [ n ] @ num

        let pop (num : int list) f = [ f num.Tail.Head num.Head ] @ num.Tail.Tail

 

        match tokens with

        | Num(n) :: rest -> eval (push num n) rest

        | Op(Add) :: rest -> eval (pop num (+)) rest

        | Op(Sub) :: rest -> eval (pop num (-)) rest

        | Op(Mul) :: rest -> eval (pop num (*)) rest

        | Op(Div) :: rest -> eval (pop num (/)) rest

        | [] -> num

    

    expression.ToCharArray() // (8) int

    |> Array.toList

    |> List.map charToToken

    |> groupNum

    |> toPostfix []

    |> eval []

    |> List.head

 

calc "1-2-3" // (9) int

  • בשורה (1) מוגדר הטיפוס Op שיכולים להיות לו 4 ערכים Add Sub Mul או Div.
  • בשורה (2) מוגדר הטיפוס Token שיכול להיות או Num עם מספר או Op אם אחד הערכים מטיפוס Op.
  • בשורה (3) מוגדרת הפונקציה calc שמקבלת מחרוזת שמכילה ביטוי מתמטי שמורכב ממספרים שלמים ואת הפעולות + – * / ומחזירה את התוצאה של החישוב.
  • בשורה (4) מוגדרת הפונקציה charToToken שממפה בעזרת Pattern Matching בין תו(Char) לערכים מהטיפוס Token.
  • בשורה (5) מוגדרת הפונקציה groupNum שמאחדת כל קבוצה של Num רצופים ב Num אחד שמכיל את המספר המלא בפונקציה זו ניתן לראות את המורכבות של התבנית שניתן לפעול לפיה האופרטור :: אומר ששוברים את הרשימה לראש (Head) וזנב(Tail) לדוגמא:
[ Num(1); Num(2); Num(3) ] -> groupNum -> [ Num(123) ]

  • בשורה (6) מוגדרת הפונקציה toPostfix שהופכת את הביטוי שרשמנו לביטוי שניתן לחשב בעזרת מחסנית בדומה למה שה CLR מבצע חישובים לדוגמא:
[ Num(1); Op(Add); Num(2); Op(Mul); Num(3) ] -> toPostfix -> [ Num(1); Num(2); Num(3); Op(Mul); Op(Add) ]

  • בשורה (7) מוגדרת הפונקציה eval שמחשבת את הביטוי בעזרת שתי פונקציות פנימיות push שמשרשרת לראש המערך את המספר החדש ו pop שמקבלת פונקציה שפועלת על שני מספרים ומחזירה מספר ומוציאה מהרשימה את שני המספרים הראשונים מפעילה עליהם את הפונקציה ומשרשרת את התוצאה למערך ללא שני האיברים הראשונים.
    בשורה (8) בונים את כל התהליך ומחזירים את התשובה.

בדוגמה זו ניתן להבחין בכמה דברים חשובים בהבדלים בין F# ל C#:

  1. התחביר של השפה הוא מינימלי ביותר כל הדוגמא לוקחת 50 שורות קוד.
  2. הקוד שממדל את המידע מופרד מהקוד של הלוגיקה בשונה מ C# ותכנות מונחה עצמים.
  3. כתוצאה מכך ומהשימוש ב Pattern Matching בכל שינוי במודל ה Compiler של F# מודיע ב Warning שלא טופלו כל האפשרויות(לפעמים גם אומר איזה אפשרויות לא טופלו) בשונה מ C# אם נשתמש ב if ה Compiler לא ידע להודיע על כך.
  4. התחביר של Descriminated Unoin קצר וקריא ובעזרתו ניתן למדל מבני נתונים היררכיים בקלות לעומת C# שצריך להשתמש בירושות והכלות וקשה לתפוס את כל העץ באותו עמוד.

לסיכום

יש עוד הרבה מה ללמוד על F# ותכנות פונקציונלי יש ספרים וקורסים בנושא והשפה צוברת תאוצה נכון ליוני 2014 TIOBE דירג את F# במקום 15. וניתן להשתמש ב F# בכל הספריות של .NET וניתן לשלב באותו Solution פרויקט C# ו F# ב Visual Studio כך שרמת ההתאקלמות מהירה.

בהצלחה.

 

יש לכם שאלות נוספות בפיתוח בעולם הדוטנט? כנסו לפורום שלנו בעברית והתייעצו עם מומחי הקהילה!

 

avi-avni-sela-groupהפוסט נכתב על ידי אבי אבני, ראש צוות ומפתח בכיר בקבוצת סלע,  מתמחה בתשתיות, ביצועים וארכיטקטורה,חי ונושם קוד 24 שעות.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *