מבוא לתכנות של קבוצת בטא/המדריך המהיר של עופר לשפת ג'וליה/פיצפוצים

פיצפוצים?

עריכה

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

טיפוסים על הקירות

עריכה

אמנם לא חייבים להשתמש בהם, אבל תמיד טוב לדעת: איזה טיפוסים יש לנו בג'וליה?
אנחנו כבר מכירים את Int64 ואת Int32 השלמים. בואו נראה מה קורה עם מחרוזות:

julia> 👻 = "boo"
"boo"
julia> typeof(👻)
String

ומה עם אות יחידה במרכאה אחת?

julia> 👽 = 'b'
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
julia> typeof(👽)
Char

אכן, בג'וליה char הוא חיה אחרת לחלוטין מString.

יש היררכיה לסוגים האלו. איך רואים אותה? כתוב בפרק של האינטרוספקשן.

אינטרוספקשן. וזה.

עריכה

Introspection היא הסתכלות לתוך מרכיבי התוכנה כדי להבין מה בשם החזיז ורעם קורה שם. נתחיל.

משתנה חילזון:

julia> 🐌 = 3
3

מה הטיפוס של החילזון?

julia> typeof(🐌)
Int64

ומה הטיפוס של Int64?

julia> typeof(Int64)
DataType

רגע רגע רגע. מה זאת אומרת טיפוס של Int64? זה משתנה?!
מסתבר שכן. משתנה מסוג Datatype.

מוזר? חכו שתגלו מה הטיפוס של Datatype...
(לבד. תגלו לבד. לא הכל בכפית.)

וזה עוד לא הכל!
הInt64 הוא לא סתם DataType, הוא DataType מסוג Signed. הנה, תראו:

julia> supertype(Int64)
Signed

כן, יש לו סופר-טיפוס! שזה כמו סופרמן רק לא. זה יותר הטיפוס-אבא שלו, ושניהם ממשפחת DataType.
ומי הסופר-טיפוס של Signed?
למה ללכת אחד אחד, בואו נראה את כל השושלת של Int64.

julia> supertypes(Int64)
(Int64, Signed, Integer, Real, Number, Any)

שזה אומר שהסופר-טיפוס שלו הוא Signed, והסופר טיפוס של signed הוא Integer, וככה עד Any שנברא עם ג'וליה.

מסקנה: לכל טיפוס יש טיפוס ויש גם סופר טיפוס, שהוא מאותו טיפוס כמו הטיפוס.
שזה כואב בעין ובלתי ברור בעליל. מזל שאפשר לראות את זה כעץ, שיהיה ברור בעליל ושמח בעין!

ברור בעליל ושמח בעין עם Term.jl

עריכה

Term.jl היא ספריה שמציירת דברים יפים לטרמינל. זאת ההגדרה הרשמית. סוג של.
אז נשתמש בה.

julia> using Term

אם אין לכם אותו, תנו לג'וליה להוריד אותו.

ואז מראים את העץ:

julia> Term.typestree(Int64)
╭────────────── Types hierarchy ───╮
                                  
  Number                          
 ━━━━━━━━                         
                                 
    ├── Complex                   
    └── Real                      
        ├── Rational              
        ├── AbstractIrrational    
        ├── AbstractFloat         
        └── Integer               
            ├── Signed            
               ├── Int16         
               ├── Int8          
               ├── BigInt        
               ├── Int64         
               ├── Int128        
               └── Int32         
            ├── Unsigned          
            └── Bool              
                                  
╰──────────────────────────────────╯

הו, שמח בעין. זה הכי גרפיקה שיש בטרמינל בעולם הזה.

נחזור על המסקנה: לכל טיפוס (במקרה שלנו Int64) יש טיפוס (הטיפוס של Int64 הוא Datatype), ויש גם סופר טיפוס (במקרה שלנו Signed), שהוא מאותו טיפוס Datatype כמו הטיפוס Int64. עכשיו ברור.


(להמשיך מכאן)


איך עשית את זה?

עריכה

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

julia> \:snail<tab>\tilde<tab>

(זה כל כך מוזר שאני אפילו לא יכול להדביק פה את התוצאה).

אבל מה אם רואים סימן שממש רוצים להשתמש בו, ולא יודעים מה הקוד להשתמש בו? נגיד הסימן לארץ שנראית כמו זחל 🗾?

כאן הREPL בא לעזרתנו. כותבים בו סימן שאלה, מה שמכניס אותנו למצב עזרה, ואז מדביקים שם את הסימן המגניב וEnter. הREPL יסביר לנו מה זה הדבר הזה:

help?> 🗾
"🗾" can be typed by \:japan:<tab>

וגם יוסיף בסוף שהוא לא מצא את זה במדריכים שלו, אבל שיהיה לו בהצלחה עם זה.

אה, וכמובן יש את הדף של ג'וליה עם כל סימני היוניקוד שהיא מסכימה להראות, זה חלק מהתיעוד.

סחוג בפיתה קבוע

עריכה

יש בג'וליה גם קבועים, נגיד הסחוג בפיתה הזה:

julia> const 🥙 = 7

ואפשר לשנות את הערך של הקבוע הזה (למרות שג'וליה לא תשמח):

julia> 🥙 = 4
WARNING: redefinition of constant 🥙. This may fail, cause incorrect answers, or produce other errors.
4

רגע, איך זה מצליח? הרי זה const.
אז הסיפור הוא שמה שקבוע כאן הוא הסוג של המשתנה, ולא הערך שלו. אם ננסה להכניס מחרוזת, למשל, זה לא יעבוד:

julia> 🥙 = "foo"
ERROR: invalid redefinition of constant d
Stacktrace:
 [1] top-level scope
   @ REPL[20]:1


בואו נדייק רגע, זה לא הסוג של המשתנה שקבוע זה הbinding, ההצמדה. לחצו על "הצגה" להסבר המלא.
יעני, סחוג בפיתה הוצמד למשתנה בזיכרון (שבתוכו יש ערכים), וconst אומר שזה המשתנה שלו וזהו ואי אפשר לשנות. אפשר לשנות רק את הערכים שבתוך המשתנה הזה. אז אם אי אפשר לגרום לסחוג בפיתה להצביע למשהו אחר, אז הוא תמיד יחזיק רק ערכים מהסוג שהוצמד אליו. זהו, סיימנו לדייק רגע.


והנה כמה קבועים שהוגדרו מראש בג'וליה, שיהיה כיף למתימטיקאים ולאנשים שאוהבים מספרי גירסה:

julia> pi
π = 3.1415926535897...

julia> VERSION
v"1.8.2"

מלטיפל דיספאץ'

עריכה

איזה שם מפוצץ. Multiple Dispatch.

בגדול זה אומר שמה שפונקציה תבצע תלוי בסוג ומספר הפרמטרים שלה. שזה תיאור די לא ברור.
אז ננסח אחרת: זה אומר שאם אתם מגדירים כמה פונקציות עם אותו שם אבל לכל אחת יש סוגי פרמטרים אחרים, או מספר אחר של פרמטרים, מה שיתקבל זה למעשה פונקציה אחת שיש לה כמה מתודות (כן, תשכחו מהמושגים הobject oriented שאולי הכרתם, כאן זה עולם אחר.) והפרמטרים שישתמשו בהם כדי לקרוא לפונקציה יקבעו מי מהמתודות למעשה תפעל.

וכאן צריך דוגמה, אחרת אף אחד לא יבין כלום.
אז הנה ארבע פונקציות:

julia> function foo(x::Int32)
           x*3
       end
foo (generic function with 1 method)
julia> function foo(x::Float64)
           x + 1
       end
foo (generic function with 2 methods)
julia> function foo(y::String)
           "Hi " * y
       end
foo (generic function with 3 methods)
julia> function foo(a::Int32, b::String)
           a * b * a
       end
foo (generic function with 4 methods)

אה, תראו, זה לא פונקציות. זה פונקציה אחת עם ארבע מתודות.

ואיך רואים שבאמת יש כאן פונקציה אחת עם כמה מתודות?
עושים אינטרוספקשן על המלטיפל דיספאץ'!
נסו להגיד את זה שלוש פעמים מהר. טוב, די.

אינטרוספקשן על המלטיפל דיספאץ'

עריכה

אינטרוספקשן על המלטיפל דיספאץ'!:

julia> methods(foo)
[1] foo(x::Int32) in Main at REPL[1]:1
[2] foo(x::Float64) in Main at REPL[2]:1
[3] foo(y::String) in Main at REPL[3]:1
[4] foo(a::Int32, b::String) in Main at REPL[4]:1

הו, באמת ארבע מתודות.

ואם אני חכמולוג?

עריכה

לfoo יש ארבע מתודות. בואו נוסיף עוד אחת, כמו המתודה השלישית foo(x::Int32), רק עם שם פרמטר אחר!
למה? כי אנחנו חכמולוג.

julia> function foo(y::Int32)
           y*3
       end
foo (generic function with 4 methods)

רגע, מה? היו לו ארבע, הוספנו אחת ועכשיו עדיין יש לו ארבע?! אינטרוספקשן בבקשה למה שקורה כאן:

julia> methods(foo)
# 4 methods for generic function "foo":
[1] foo(x::Float64) in Main at REPL[2]:1
[2] foo(y::String) in Main at REPL[3]:1
[3] foo(y::Int32) in Main at REPL[5]:1
[4] foo(a::Int32, b::String) in Main at REPL[4]:1

הוא פשוט החליף לו את המתודה השלישית מfoo(x::String) לfoo(y::String), במקום להוסיף. שערוריה.

אז למה הוא לא הוסיף?
כי מה שמגדיר מתודה זו החתימה שלה. והחתימה של המתודה השניה היתה והיא עדיין foo(Int32). שמות הפרמטרים הם לא חלק מהחתימה, ולכן אם נגדיר מתודה נוספת עם אותה חתימה, היא פשוט תתפוס את המקום שמתאים לחתימה שלה, ולא מעניין אותה מה שמות הפרמטרים.

זהו סיימנו להיות חכמולוג. בואו נראה כמה מידע אפשר לגרד ממתודה.

מגרדים מידע על מתודה

עריכה

אז איזה אינטרוספקשנים יש לנו, נגיד, על המתודה הראשונה של ()foo? מה אפשר לדעת עליה?

רגע, איך בכלל ניגשים אליה? זה מערך הרשימה הזאת של המתודות? אפשר להסתכל על האיברים שלה כמו מערך?
לא. כי בג'וליה קוראים לזה וקטור ולא מערך.
אז איך ניגשים, נגיד, לאיבר הראשון, למתודה הראשונה? כמו בוקטור? זה וקטור הרשימה הזאת?

typeof(methods(foo))
Base.MethodList

זה לא וקטור, זה רשימת מתודות. ספציפי למדי.
אולי יש לנו מזל וזה subtype של וקטור (אז וקטור זה הsuper type שלו) ולכן מתנהג כמו וקטור? בואו נבדוק בצורה ברורה בעליל ושמחה בעין.

julia> Term.typestree(Base.MethodList)
╭─────────────────────────────────────── Types hierarchy ───╮
                                                           
  AbstractVector                                           
 ━━━━━━━━━━━━━━━━                                          
                                                          
        ├── Core.Compiler.AbstractRange                    
        ├── PermutedDimsArray{Method, 1}                   
        ├── SubArray{Method, 1}                            
        ├── Base.LogicalIndex                              
        ├── Base.ReinterpretArray{Method, 1, S} where S    
        ├── DenseVector                                    
        ├── Core.Compiler.MethodList                       
        ├── SparseArrays.AbstractSparseVector              
        ├── AbstractRange                                  
        ├── Base.MethodList                                
        ├── Test.GenericArray{Method, 1}                   
        └── Base.ReshapedArray{Method, 1}                  
                                                           
╰───────────────────────────────────────────────────────────╯

כן, זה סוג של וקטור. אז ננסה לגשת לאיבר הראשון:

julia> methods(foo)[1]
foo(x::Int32) in Main at REPL[11]:1

ניצחון, יש לנו איבר ראשון.
אז מה אפשר לדעת עליו?:

julia> fieldnames(typeof(methods(foo)[1]))
(:name, :module, :file, :line, :primary_world, :deleted_world, :sig, :specializations, :speckeyset, :slot_syms, :external_mt, :source, :unspecialized, :generator, :roots, :root_blocks, :nroots_sysimg, :ccallable, :invokes, :recursion_relation, :nargs, :called, :nospecialize, :nkw, :isva, :pure, :is_for_opaque_closure, :constprop, :purity)

שלום המון מידע!

מה זה nargs?

methods(foo)[1].nargs
2

זה אומר שיש לmethod הזה שני פרמטרים.

ומה זה sig?

julia> methods(foo)[1].sig
Tuple{typeof(foo), Int32}

זאת החתימה של המתודה! זו מתודת foo עם פרמטר Int32.
וואו, זה המון דברים נורא מעניינים על מתודות.

רוצים לדעת איזה מידע נמצא בכל השדות האחרים? זה בהערות של julia.h בקוד של ג'וליה. בהצלחה עם זה. כשתבינו תגידו לנו.

כיף להגיד מלטיפל דיספאץ', ספר לי עוד עליו

עריכה

המאמר המצוין של מאתיס קוקס עושה בדיוק את זה.

יש בג'וליה do, שהוא למעשה דרך יפה יותר לעבוד עם פרמטר פונקציה.
מה זה אומר? בואו קודם נבין מה זה פונקציה שיש לה פרמטר פונקציה.

פונקציה שיש לה פרמטר פונקציה

עריכה

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

julia>  map(ceil, [1.4, 3.7, 4.2])
2.0
4.0
5.0

יופי. השתמשנו בפונקציה ceil שקיימת בג'וליה, והיינו רק צריכים לכתוב את השם שלה.
אבל מה אם אנחנו רוצים לכתוב בעצמנו את הפונקציה שבה map משתמשת?

נגיד, אני רוצה פונקציה שמכפילה ב2. אז אני אצטרך לכתוב:

julia> function foo(a::Float64) ::Float64
           return a * 2
       end
foo (generic function with 1 method)
julia> map(foo, [1.4, 3.7, 4.2])
3-element Vector{Float64}:
 2.8
 7.4
 8.4

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

julia> map((a::Float64)->a * 2, [1.4, 3.7, 4.2])
3-element Vector{Float64}:
 2.8
 7.4
 8.4

וכאן אנחנו מגיעים לdo.

אז מסתבר שאני יכול גם לכתוב את זה בדרך שלישית, בעזרת do:

julia> map([1.4, 3.7, 4.2]) do (a::Float64)
           return a * 2
       end
3-element Vector{Float64}:
 2.8
 7.4
 8.4

והדרך השלישית הזאת מאפשרת לי לכתוב פונקציה אנונימית, אבל לכתוב אותה יפה בכמה שורות כמו בנאדם, ולא בשורה אחת כמו מישהו שאף אחד בחיים לא ירצה לתחזק את הקוד שלו וכל מי שיצטרך להחליף אותו בתפקיד יקלל אותו לנצח נצחים.
מזל שיש do.

לפרטים נוספים על do ופונקציות אנונימיות: התיעוד של ג'וליה על פונקציות אנונימיות.

שרברבות

עריכה

מסתבר שיש בג'וליה פייפים (Pipes), צינורות. לא מכירים? סימן שלא עבדתם מספיק בלינוקס.
פייפים זה מצויין כשצריכים להפעיל רצף של פונקציות (רצוי עם פרמטר יחיד), ולא רוצים לטבוע בים של סוגריים.

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

repeat(reverse(lowercasefirst(uppercase(rstrip("foo  ")))), 2)
"OOfOOf"

אבל אפשר גם לעשות את זה יותר קל לצופה עם פייפים:

"foo  " |> rstrip |> uppercase |> lowercasefirst |> reverse |> x -> repeat(x,2)
"OOfOOf"

כן, יש שם פונקציה אנונימית, למקרה שתרצו לשאול איך מריצים כשיש יותר מפרמטר אחד.

שרברבות בשידור ישיר

עריכה

ועכשיו לשלב הבא: אפשר להפעיל את השרברבות הזאת ביחד עם broadcasting, כך שכל הטוב הזה יפעל על כל האיברים בו זמנית:

split("Can One Operator Lie?") .|> first .|> lowercase |> join |> uppercasefirst
"Cool"

תנו לי עוד כוחות שרברבות!

עריכה

טוב. הנה: pipe.jl.

unit testing

עריכה

בדיקות היחידה מובנות בתוך השפה, כיף כיף כיף. רק צריך את המודול Test:

using Test

בדיקות נעשות ככה:

@test uppercase("a") == "A"
Test Passed

או כשהן נכשלות:

@test uppercase("a") == "b"
Test Failed at C:\foo\test.jl:3
  Expression: uppercase("a") == "b"
   Evaluated: "A" == "b"
ERROR: LoadError: There was an error during testing
in expression starting at C:\foo\test.jl:3

אפשר גם להגדיר גוש של בדיקות:

@testset "lots o tests" begin
	@test uppercase("a") == "A"
	@test sin(34) < sin(20)
	@test reverse("foo") == "oof"
end
Test Summary: | Pass  Total  Time
lots o tests  |    3      3  0.1s

לגלות את האמת

עריכה

שלוש פונקציות עוזרות למצוא ביטויי אמת בוקטורים:

  • findfirst - מוצאת את האמת הראשונה.
  • findall - מוצאת את כל מה שאמת.
  • findnext - מוצאת את האמת הבאה.

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

vec = [false, false, true, false]
4-element Vector{Bool}:
 0
 0
 1
 0

אז מה האינדקס של האמת הראשונה פה?

julia> findfirst(vec)
3

כן. אמת.
אבל מה אפשר לעשות עם זה?

להחיל פונקציות כמובן.
נגיד, לאיזה איברים במערך הזה, החל מאיבר 3, יש ריבוע קטן מ27?

vec = [2, 3, 7, 5]
findnext(x -> x^2 < 27, vec, 3)
 4

לאינדקסים 4 יש ריבוע כזה.
שועלי C ונחשי פייתון יציינו בקול רוטן שאפשר לעשות את זה גם עם סידרה של פונקציות אחרות. נכון, אבל ככה יותר קל להבין מה רצה המתכנת.

אופרטור סחוג בפיתה

עריכה

אין אופרטור סחוג בפיתה. אבל יש אופרטורים מגניבים אחרים. נגיד in\ שיוצא ומראה בדוגמה ש3 אכן נמצא במערך:

3  [2, 3, 4]
true

ג'וליה, את לא PHP!

עריכה

ג'וליה נותנת גם כל מיני פונקציות נוחות מהסוג שהפכו את PHP לכל כך כיפי וכל כך חסר עקיבות פנימית.

נגיד, פירוק מספר לספרות:

julia> digits(304)
3-element Vector{Int64}:
 4
 0
 3

ספירת מספר הספרות:

julia> ndigits(304)
3

בדיקה אם זה אחד, כי מה היינו עושים בלי זה (כנראה היינו עושים אופרטור שוויון, מה נסגר):

julia> isone(2)
false
julia> isone(1)
true

ובדיקת ריקנות, כי למה לא:

julia> isempty([1, 2, 3])
false
julia> isempty([])
true

כן, הisempty עובד גם לintים, אבל זה כנראה מפענח אותם כתנאי. לא דיון לעכשיו.

worlds colliding

עריכה

אז איך מריצים קוד ג'וליה מתוך C?

תחזיקו חזק, הולך ליפול לכם הראש. זה כבר מצדיק דף נפרד.

דאטה סיינס

עריכה

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

בשביל נושא הטבלאות והתצוגה יש לנו את הספר החופשי Julia Data Scientce של סטורופולי, הויצר ואלונסו מ2021.

בכל מה שנוגע ללמידת מכונה, נכון ל2022 יש ספריות ג'וליה לזה, אבל לצערנו יש שם בעיות של איכות שלא מאפשרות לסמוך עליהן בעיניים עצומות. אז נושא למידת מכונה בג'וליה עדיין לא עבד לגמרי חלק ב2022, אבל שווה לבדוק בפורומים מה הסטטוס הנוכחי.


פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.

למידע נוסף

עריכה