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
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:
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
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:
- kode semakin pendek
map()
atau reduce()
tunjukkan semantik dari operasi yang sedang dilakukan, maka Anda masih perlu memahami semantik dari apa yang terjadi dalam loopmap()
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:
Fungsi yang dinamai dan anonim dapat ditugaskan ke variabel dan dikembalikan sebagai nilai:
julia> double_squared = x -> (2 * x)^2
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:
- Apa yang terjadi jika variabel bernama z didefinisikan di luar fungsi?
- 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
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)
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}})
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}})
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, π])
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.