Julia: fungsi dan struktur sebagai fungsi

Terlepas dari kenyataan bahwa konsep Julia tidak memiliki pemrograman berorientasi objek "klasik" dengan kelas dan metode, bahasa menyediakan alat abstraksi, peran kunci di mana dimainkan oleh sistem jenis dan elemen pemrograman fungsional. Mari kita bahas poin kedua secara lebih rinci.

Konsep fungsi dalam Julia mungkin yang paling mirip dengan bahasa dari keluarga Lisp (lebih tepatnya, cabang Lisp-1), dan fungsi dapat dianggap pada tiga tingkatan: sebagai subprogram, sebagai abstraksi untuk urutan tindakan tertentu, dan sebagai data yang mewakili abstraksi ini .

Level 1. Berfungsi sebagai rutinitas


Alokasi subprogram dan penugasan nama mereka sendiri telah berlangsung sejak zaman prasejarah, ketika Fortran dianggap sebagai bahasa tingkat tinggi, dan C belum ada di sana.

Dalam hal ini, produk Julia adalah standar. "Fitur" dapat disebut fakta bahwa secara sintaksis tidak ada pembagian ke dalam prosedur dan fungsi. Terlepas dari apakah subrutin dipanggil untuk mendapatkan nilai atau hanya untuk melakukan beberapa tindakan pada data, itu disebut fungsi.

Definisi fungsi dimulai dengan function kata kunci, diikuti oleh daftar argumen, urutan perintah dalam tanda kurung, dan kata end mengakhiri definisi:

 """ sum_all(collection) Sum all elements of a collection and return the result """ function sum_all(collection) sum = 0 for item in collection sum += collection end sum end 

Sintaks dibedakan oleh perilaku yang diwarisi dari Lisp: untuk pengembalian "normal" dari suatu fungsi, kata return tidak diperlukan: nilai ekspresi terakhir dihitung sebelum end dikembalikan. Dalam contoh di atas, nilai sum variabel akan dikembalikan. Dengan demikian, return dapat digunakan sebagai penanda perilaku khusus suatu fungsi:

 function safe_division(number, divisor) if divisor == 0 return 0 end number / divisor end #    function safe_division1(number, divisor) if divisor == 0 0 #             else number / divisor end end 

Untuk fungsi dengan definisi pendek, ada sintaks singkat yang mirip dengan notasi matematika. Jadi, perhitungan panjang sisi miring sepanjang kaki dapat didefinisikan sebagai berikut:

 hypotenuse(a, b) = sqrt(a^2 + b^2) 

Divisi "aman" menggunakan operator ternary dapat ditulis sebagai berikut:

 safe_division(number, divisor) = divisor == 0 ? 0 : number / divisor 

Seperti yang Anda lihat, tidak perlu menentukan tipe untuk argumen fungsi. Mengingat cara kerja kompiler Julia JIT, mengetik bebek tidak selalu menghasilkan kinerja yang buruk.

Ketika saya mencoba menunjukkan dalam artikel sebelumnya , kompiler Julia dapat menyimpulkan jenis hasil pengembalian dengan jenis argumen input. Karena itu, misalnya, fungsi safe_division memerlukan modifikasi minimal untuk operasi cepat:

 function safe_division(number, divisor) if divisor == 0 return zero(number / divisor) end number / divisor end 

Sekarang, jika jenis kedua argumen diketahui pada tahap kompilasi, jenis hasil yang dikembalikan juga ditampilkan secara jelas, karena fungsi zero(x) mengembalikan nilai nol dari jenis yang sama dengan argumennya (dan membaginya dengan nol, sesuai dengan IEEE 754 , memiliki nilai yang dapat direpresentasikan dengan sempurna dalam format angka floating-point).

Fungsi dapat memiliki sejumlah tetap argumen posisi, argumen posisi dengan nilai default, argumen bernama, dan sejumlah variabel argumen. Sintaks:

 #    function hello(name) println("Hello, ", name) end #      #           function greeting_d(name, greeting = "Hello") println(greeting, ", ", name) end #       #          #       function greeting_kw(name; greeting = "Hello") println(greeting, ", ", name) end #  greeting   ,        function greeting_oblkw(name; greeting) println(greeting, ", ", name) end #      #  ,   ,      names function greeting_nary(greeting, names...) print(greeting) for name in names print(", ", name) end print('\n') end julia> hello("world") Hello, world julia> greeting_d("world") Hello, world julia> greeting_d("Mr. Smith", "How do you do") How do you do, Mr. Smith julia> greeting_kw("Mr. Smith") Hello, Mr. Smith julia> greeting_kw("mom", greeting = "Hi") Hi, mom julia> greeting_oblkw("world") ERROR: UndefKeywordError: keyword argument greeting not assigned Stacktrace: [1] greeting_oblkw(::String) at ./REPL[23]:3 [2] top-level scope at none:0 julia> greeting_oblkw("mom", greeting = "Hi") Hi, mom julia> greeting_nary("Hi", "mom", "dad", "everyone") Hi, mom, dad, everyone 

Level 2. Berfungsi sebagai Data


Nama fungsi tidak hanya dapat digunakan dalam panggilan langsung, tetapi juga sebagai pengidentifikasi yang terkait dengan prosedur untuk mendapatkan nilai. Sebagai contoh:

 function f_x_x(fn, x) fn(x, x) end julia> f_x_x(+, 3) 6 # +(3, 3) = 3+3 = 6 julia> f_x_x(*, 3) 9 # *(3, 3) = 9 julia> f_x_x(^, 3) 27 # ^(3, 3) = 3^3 = 27 julia> f_x_x(log, 3) 1.0 # log(3, 3) = 1 

Fungsi "klasik" yang mengambil argumen fungsional adalah map , reduce dan filter .

map(f, x...) menerapkan fungsi f ke nilai semua elemen dari x (atau tupel elemen-i) dan mengembalikan hasilnya sebagai koleksi baru:

 julia> map(cos, [0, π/3, π/2, 2*π/3, π]) 5-element Array{Float64,1}: 1.0 0.5000000000000001 6.123233995736766e-17 -0.4999999999999998 -1.0 julia> map(+, (2, 3), (1, 1)) (3, 4) 

reduce(f, x; init_val) "mengurangi" koleksi ke nilai tunggal, "memperluas" rantai f(f(...f(f(init_val, x[1]), x[2])...), x[end]) :

 function myreduce(fn, values, init_val) accum = init_val for x in values accum = fn(accum, x) end accum end 

Karena itu tidak benar-benar ditentukan dalam urutan apa array akan melewati selama pengurangan, atau apakah fn(accum, x) atau fn(x, accum) akan fn(x, accum) , reduksi akan memberikan hasil yang dapat diprediksi hanya dengan operator komutatif atau asosiatif, seperti penambahan atau multiplikasi.

filter(predicate, x) mengembalikan array elemen x yang memenuhi predikat predicate :

 julia> filter(isodd, 1:10) 5-element Array{Int64,1}: 1 3 5 7 9 julia> filter(iszero, [[0], 1, 0.0, 1:-1, 0im]) 4-element Array{Any,1}: [0] 0.0 1:0 0 + 0im 

Menggunakan fungsi tingkat tinggi untuk operasi pada array daripada menulis loop memiliki beberapa keuntungan:

  1. kode semakin pendek
  2. map() atau reduce() tunjukkan semantik dari operasi yang sedang dilakukan, maka Anda masih perlu memahami semantik dari apa yang terjadi dalam loop
  3. map() memungkinkan kompiler untuk memahami bahwa operasi pada elemen array independen oleh data, yang memungkinkan optimasi tambahan untuk diterapkan

Level 3. Berfungsi sebagai Abstraksi


Seringkali dalam map() atau filter() Anda perlu menggunakan fungsi yang belum ditetapkan namanya sendiri. Julia dalam kasus ini memungkinkan Anda untuk mengekspresikan abstraksi operasi pada argumen, tanpa memasukkan nama Anda sendiri untuk urutan ini. Abstraksi semacam itu disebut fungsi anonim , atau fungsi lambda (karena dalam tradisi matematika fungsi tersebut dilambangkan dengan huruf lambda). Sintaks untuk tampilan ini adalah:

 #   square(x) = x^2 #   x -> x^2 #   hypot(a, b) = sqrt(x^2 + y^2) #   -    ,    , #              (x, y) -> sqrt(x^2 + y^2) #   fortytwo() = 42 #   () -> 42 julia> map(i -> map(x -> x^i, 1:5), 1:5) 5-element Array{Array{Int64,1},1}: [1, 2, 3, 4, 5] [1, 4, 9, 16, 25] [1, 8, 27, 64, 125] [1, 16, 81, 256, 625] [1, 32, 243, 1024, 3125] 

Fungsi yang dinamai dan anonim dapat ditugaskan ke variabel dan dikembalikan sebagai nilai:

 julia> double_squared = x -> (2 * x)^2 #17 (generic function with 1 method) julia> double_squared(5) 100 

Lingkup variabel dan penutupan leksikal


Biasanya, mereka mencoba untuk menulis fungsi sedemikian rupa sehingga semua data yang diperlukan untuk perhitungan diperoleh melalui argumen formal, yaitu nama variabel apa pun yang muncul dalam tubuh adalah nama argumen formal atau nama variabel yang dimasukkan di dalam tubuh fungsi.

 function normal(x, y) z = x + y x + y * z end function strange(x, y) x + y * z end 

Tentang fungsi normal() , kita dapat mengatakan bahwa di dalam tubuhnya semua nama variabel terkait , mis. jika kita di mana-mana (termasuk daftar argumen) mengganti "x" dengan "m" (atau pengidentifikasi lainnya), "y" dengan "n", dan "z" dengan "sum_of_m_and_n", arti dari ekspresi tidak akan berubah. Dalam fungsi strange() , nama z tidak terkait , mis. a) artinya dapat berubah jika nama ini diganti dengan yang lain, dan b) kebenaran fungsi tergantung pada apakah variabel dengan nama "z" didefinisikan pada saat fungsi dipanggil.

Secara umum, fungsi normal() juga tidak begitu bersih:

  1. Apa yang terjadi jika variabel bernama z didefinisikan di luar fungsi?
  2. Karakter + dan *, pada kenyataannya, juga merupakan pengidentifikasi yang tidak terkait.

Dengan poin 2, tidak ada yang bisa dilakukan selain untuk menyetujui - adalah logis bahwa definisi semua fungsi yang digunakan dalam sistem harus ada, dan kami berharap bahwa arti sebenarnya sesuai dengan harapan kami.

Poin 1 kurang jelas dari yang terlihat. Faktanya adalah bahwa jawabannya tergantung pada di mana fungsi tersebut didefinisikan. Jika itu didefinisikan secara global, maka z di dalam normal() akan menjadi variabel lokal, mis. bahkan jika ada variabel global z nilainya tidak akan ditimpa. Jika definisi fungsi ada di dalam blok kode, maka jika ada definisi sebelumnya dari z dalam blok ini, nilai variabel eksternal akan diubah.

Jika badan fungsi berisi nama variabel eksternal, maka nama ini dikaitkan dengan nilai yang ada di lingkungan tempat fungsi itu dibuat. Jika fungsi itu sendiri diekspor dari lingkungan ini (misalnya, jika dikembalikan dari fungsi lain sebagai nilai), maka itu "menangkap" variabel dari lingkungan internal, yang tidak lagi dapat diakses di lingkungan baru. Ini disebut penutupan leksikal.

Penutupan terutama berguna dalam dua situasi: ketika Anda perlu membuat fungsi sesuai dengan parameter yang diberikan dan ketika Anda membutuhkan fungsi yang memiliki keadaan internal.

Pertimbangkan situasi dengan fungsi yang merangkum keadaan internal:

 function f_with_counter(fn) call_count = 0 ncalls() = call_count # invoke()  ,     #    ,  ncalls() function invoke(args...) call_count += 1 fn(args...) end #         # call_count     , #   invoke()  call_count()        (call = invoke, call_count = ncalls) end julia> abscount = f_with_counter(abs) (call = getfield(Main, Symbol("#invoke#22")){typeof(abs)}(abs, Core.Box(0)), call_count = getfield(Main, Symbol("#ncalls#21"))(Core.Box(0))) julia> abscount.call_count() 0 julia> abscount.call(-20) 20 julia> abscount.call_count() 1 julia> abscount.call(im) 1.0 julia> abscount.call_count() 2 

Studi kasus: semua polinomial yang sama


Dalam artikel sebelumnya, penyajian polinomial sebagai struktur dipertimbangkan. Secara khusus, salah satu struktur penyimpanan adalah daftar koefisien, dimulai dengan yang termuda. Untuk menghitung polinom p pada titik x diusulkan untuk memanggil fungsi evpoly(p, x) , yang menghitung polinom sesuai dengan skema Horner.

Kode definisi penuh
 abstract type AbstractPolynomial end """ Polynomial <: AbstractPolynomial Polynomials written in the canonical form --- Polynomial(v::T) where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) Construct a `Polynomial` from the list of the coefficients. The coefficients are assumed to go from power 0 in the ascending order. If an empty collection is provided, the constructor returns a zero polynomial. """ struct Polynomial<:AbstractPolynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) coeff = isempty(v) ? (0.0,) : tuple([Float64(x) for x in v]...) return new(length(coeff)-1, coeff) end end """ InterpPolynomial <: AbstractPolynomial Interpolation polynomials in Newton's form --- InterpPolynomial(xsample::Vector{<:Real}, fsample::Vector{<:Real}) Construct an `InterpPolynomial` from a vector of points `xsample` and corresponding function values `fsample`. All values in `xsample` must be distinct. """ struct InterpPolynomial<:AbstractPolynomial degree::Int xval::NTuple{N, Float64} where N coeff::NTuple{N, Float64} where N function InterpPolynomial(xsample::X, fsample::F) where {X<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}, F<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} if !allunique(xsample) throw(DomainError("Cannot interpolate with duplicate X points")) end N = length(xsample) if length(fsample) != N throw(DomainError("Lengths of X and F are not the same")) end coeff = [Float64(f) for f in fsample] for i = 2:N for j = 1:(i-1) coeff[i] = (coeff[j] - coeff[i]) / (xsample[j] - xsample[i]) end end new(N-1, ntuple(i -> Float64(xsample[i]), N), tuple(coeff...)) end end function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end function evpoly(p::Polynomial, z::Real) ans = p.coeff[end] for idx = p.degree:-1:1 ans = p.coeff[idx] + z * ans end return ans end function evpoly(p::InterpPolynomial, z::Real) ans = p.coeff[p.degree+1] for idx = p.degree:-1:1 ans = ans * (z - p.xval[idx]) + p.coeff[idx] end return ans end function Base.:+(p1::Polynomial, p2::Polynomial) #    ,      deg = max(p1.degree, p2.degree) coeff = zeros(deg+1) coeff[1:p1.degree+1] .+= p1.coeff coeff[1:p2.degree+1] .+= p2.coeff Polynomial(coeff) end function Base.:+(p1::InterpPolynomial, p2::InterpPolynomial) xmax = max(p1.xval..., p2.xval...) xmin = min(p1.xval..., p2.xval...) deg = max(p1.degree, p2.degree) #         #       xmid = 0.5 * xmax + 0.5 * xmin dx = 0.5 * (xmax - xmin) / cos(0.5 * π / (deg + 1)) chebgrid = [xmid + dx * cos((k - 0.5) * π / (deg + 1)) for k = 1:deg+1] fsample = [evpoly(p1, x) + evpoly(p2, x) for x in chebgrid] InterpPolynomial(chebgrid, fsample) end function Base.:+(p1::InterpPolynomial, p2::Polynomial) xmax = max(p1.xval...) xmin = min(p1.xval...) deg = max(p1.degree, p2.degree) xmid = 0.5 * xmax + 0.5 * xmin dx = 0.5 * (xmax - xmin) / cos(0.5 * π / (deg + 1)) chebgrid = [xmid + dx * cos((k - 0.5) * π / (deg + 1)) for k = 1:deg+1] fsample = [evpoly(p1, x) + evpoly(p2, x) for x in chebgrid] InterpPolynomial(chebgrid, fsample) end function Base.:+(p1::Polynomial, p2::InterpPolynomial) p2 + p1 end 


Representasi polinomial dalam bentuk struktur tidak sepenuhnya sesuai dengan pemahaman intuitifnya sebagai fungsi matematika. Tetapi dengan mengembalikan nilai fungsional, polinomial juga dapat ditentukan secara langsung sebagai fungsi. Jadi itu:

 struct Polynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) #     /     P(x) ≡ 0 coeff = isempty(v) ? (0.0,) : tuple([Float64(x) for x in v]...) #   -   new #  -    return new(length(coeff)-1, coeff) end end """ evpoly(p::Polynomial, z::Real) Evaluate polynomial `p` at `z` using the Horner's rule """ function evpoly(p::Polynomial, z::Real) ans = p.coeff[end] for idx = p.degree:-1:1 ans = p.coeff[idx] + z * ans end return ans end 

Kami mengubah definisi ini menjadi fungsi yang mengambil array / tuple koefisien dan mengembalikan fungsi aktual yang menghitung polinomial:
 function Polynomial_as_closure(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) #     /     P(x) ≡ 0 if isempty(v) return x::Real -> 0.0 end coeff = tuple(map(float, v)...) degree = length(coeff) - 1 function evpoly(z::Real) ans = coeff[end] for idx = degree:-1:1 ans = coeff[idx] + z * ans end return ans end evpoly end julia> p = Polynomial_as_closure((0, 1, 1)) # x² + x (::getfield(Main, Symbol("#evpoly#28")){Tuple{Float64,Float64,Float64},Int64}) (generic function with 1 method) julia> p(1) # ,    evpoly()! 2.0 julia> p(11) 132.0 

Demikian pula, Anda dapat menulis fungsi untuk polinomial interpolasi.

Sebuah pertanyaan penting: apakah ada sesuatu yang hilang dalam definisi baru dalam definisi sebelumnya? Sayangnya, ya - pengaturan polinomial sebagai struktur memberi petunjuk bagi kompiler, dan bagi kami, kemampuan untuk membebani operator aritmatika untuk struktur ini. Sayangnya, Julia tidak menyediakan untuk fungsi sistem tipe yang kuat.

Untungnya, dalam hal ini kita dapat mengambil yang terbaik dari kedua dunia, karena Julia memungkinkan Anda untuk membuat apa yang disebut struct yang bisa dipanggil. Yaitu Anda dapat menentukan polinomial sebagai struktur, tetapi dapat menyebutnya sebagai fungsi! Untuk definisi struktur dari artikel sebelumnya, Anda hanya perlu menambahkan:

 function (p::Polynomial)(z::Real) evpoly(p, z) end function (p::InterpPolynomial)(z::Real) evpoly(p, z) end 

Menggunakan argumen fungsional, Anda juga dapat menambahkan konstruktor eksternal dari polinomial interpolasi untuk fungsi tertentu yang dibangun dari sekumpulan titik:

 function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end 

Kami memverifikasi definisi
 julia> psin = InterpPolynomial(sin, [0, π/6, π/2, 5*π/6, π]) #   InterpPolynomial(4, (0.0, 0.5235987755982988, 1.5707963267948966, 2.6179938779914944, 3.141592653589793), (0.0, 0.954929658551372, -0.30396355092701327, -0.05805276197975913, 0.036957536116863636)) julia> pcos = InterpPolynomial(cos, [0, π/6, π/2, 5*π/6, π]) #   InterpPolynomial(4, (0.0, 0.5235987755982988, 1.5707963267948966, 2.6179938779914944, 3.141592653589793), (1.0, -0.2558726308373678, -0.36358673785585766, 0.1388799037738005, 5.300924469105863e-17)) julia> psum = pcos + psin InterpPolynomial(4, (3.141592653589793, 2.5416018461576297, 1.5707963267948966, 0.5999908074321635, 0.0), (-1.0, -1.2354929267138448, 0.03888175053443867, 0.1969326657535598, 0.03695753611686364)) julia> for x = range(0, π, length = 20) println("Error at x = ", x, ": ", abs(psum(x) - (sin(x) + cos(x)))) end Error at x = 0.0: 0.0 Error at x = 0.3490658503988659: 0.002748366490382681 Error at x = 0.6981317007977318: 0.0031870524474437723 Error at x = 1.0471975511965976: 0.006538414090220712 Error at x = 1.3962634015954636: 0.0033647273630357244 Error at x = 1.7453292519943295: 0.003570894863996865 Error at x = 2.0943951023931953: 0.007820939854677023 Error at x = 2.443460952792061: 0.004305934583281101 Error at x = 2.792526803190927: 0.00420977797025246 Error at x = 3.141592653589793: 1.1102230246251565e-16 


Kesimpulan


Kemungkinan yang dipinjam dari pemrograman fungsional di Julia memberikan bahasa yang lebih ekspresif dibandingkan dengan gaya imperatif murni. Representasi struktur dalam bentuk fungsi adalah cara merekam konsep matematika yang lebih nyaman dan alami.

Source: https://habr.com/ru/post/id451172/


All Articles