Mungkinkah memiliki fungsi currying dan variadic pada saat yang bersamaan?

13

Saya sedang berpikir tentang membuat currying dan fungsi variadic keduanya tersedia dalam bahasa pemrograman fungsional yang diketik secara dinamis, tapi saya ingin tahu apakah itu mungkin atau tidak.

Berikut ini beberapa kodesemu:

sum = if @args.empty then 0 else @args.head + sum @args.tail

yang seharusnya meringkas semua argumennya. Kemudian, jika sumitu sendiri diberi nomor, maka hasilnya adalah 0. sebagai contoh,

sum + 1

sama dengan 1, dengan asumsi bahwa +hanya dapat bekerja pada angka. Namun, meskipun sum == 0benar, summasih akan mempertahankan nilai dan properti fungsionalnya tidak peduli berapa banyak argumen yang diberikan (maka "diterapkan sebagian" dan "variadik" pada saat yang sama), misalnya, jika saya menyatakan

g = sum 1 2 3

maka gsama dengan 6, bagaimanapun, kita masih bisa menerapkan lebih lanjut g. Sebagai contoh, g 4 5 == 15itu benar. Dalam kasus ini, kami tidak dapat mengganti objek gdengan literal 6, karena meskipun mereka menghasilkan nilai yang sama ketika diperlakukan sebagai integer, mereka mengandung kode yang berbeda di dalamnya.

Jika desain ini digunakan dalam bahasa pemrograman nyata, apakah akan menimbulkan kebingungan atau ambiguitas?

Michael Tsang
sumber
1
Sebenarnya, menggunakan currying sebagai dasar bahasa berarti bahwa semua fungsi adalah unary - tidak hanya tidak ada fungsi variadik, bahkan tidak ada yang biner! Namun, program-program dalam bahasa itu masih akan terlihat seolah-olah mereka mengambil banyak argumen, dan itu berlaku untuk fungsi variadic seperti halnya untuk yang normal.
Kilian Foth
Kemudian pertanyaan saya disederhanakan menjadi "dapatkah suatu objek menjadi fungsi dan nilai non-fungsi pada saat yang sama?" Dalam contoh di atas, sumadalah 0tanpa argumen dan secara rekursif menyebut dirinya dengan argumen.
Michael Tsang
bukankah itu pekerjaan reduce?
ratchet freak
1
Lihatlah fungsi yang Anda gunakan pada args: empty, head, dan tail. Itu semua adalah fungsi daftar, menunjukkan bahwa mungkin hal yang lebih mudah dan lebih mudah untuk dilakukan adalah dengan menggunakan daftar di mana hal-hal variadik akan terjadi. (Jadi, sum [1, 2, 3]alih-alih sum 1 2 3)
Michael Shaw

Jawaban:

6

Bagaimana vararg dapat diimplementasikan? Kami membutuhkan beberapa mekanisme untuk menandai akhir dari daftar argumen. Ini bisa jadi

  • nilai terminator khusus, atau
  • panjang daftar vararg diteruskan sebagai parameter tambahan.

Kedua mekanisme ini dapat digunakan dalam konteks currying untuk mengimplementasikan varargs, tetapi pengetikan yang tepat menjadi masalah utama. Mari kita asumsikan bahwa kita berurusan dengan suatu fungsi sum: ...int -> int, kecuali bahwa fungsi ini menggunakan currying (jadi kita sebenarnya memiliki tipe yang lebih suka sum: int -> ... -> int -> int, kecuali bahwa kita tidak tahu jumlah argumen).

Kasus: nilai terminator: Membiarkan endmenjadi terminator khusus, dan Tmenjadi tipe sum. Kita sekarang tahu bahwa diterapkan endkembali fungsi: sum: end -> int, dan yang diterapkan ke int kita mendapatkan sum-seperti fungsi lain: sum: int -> T. Oleh karena itu Tadalah gabungan dari jenis: T = (end -> int) | (int -> T). Oleh penggantinya T, kita mendapatkan berbagai jenis mungkin seperti end -> int, int -> end -> int, int -> int -> end -> int, dll Namun, kebanyakan sistem tipe tidak mengakomodasi jenis tersebut.

Kasus: panjang eksplisit: Argumen pertama ke fungsi vararg adalah jumlah vararg. Jadi sum 0 : int, sum 1 : int -> int, sum 3 : int -> int -> int -> intdll Hal ini didukung dalam beberapa sistem jenis dan merupakan contoh mengetik tergantung . Sebenarnya, jumlah argumen akan menjadi parameter jenis dan bukan merupakan parameter biasa - itu tidak masuk akal untuk arity fungsi bergantung pada nilai runtime, s = ((sum (floor (rand 3))) 1) 2jelas sakit-mengetik: mengevaluasi ini baik s = ((sum 0) 1) 2 = (0 1) 2, s = ((sum 1) 1) 2 = 1 2atau s = ((sum 2) 1) 2 = 3.

Dalam praktiknya, tidak satu pun dari teknik ini harus digunakan karena mereka rawan kesalahan, dan tidak memiliki tipe (bermakna) dalam sistem tipe umum. Sebaliknya, hanya lulus daftar nilai sebagai salah satu Penyempitan: sum: [int] -> int.

Ya, dimungkinkan untuk suatu objek muncul sebagai fungsi dan nilai, misalnya dalam sistem tipe dengan paksaan. Membiarkan summenjadi SumObj, yang memiliki dua paksaan:

  • coerce: SumObj -> int -> SumObjmemungkinkan sumuntuk digunakan sebagai fungsi, dan
  • coerce: SumObj -> int memungkinkan kita untuk mengekstrak hasilnya.

Secara teknis, ini adalah variasi dari kasus nilai terminator di atas, dengan T = SumObj, dan coercemenjadi un-wrapper untuk tipe tersebut. Dalam banyak bahasa berorientasi objek, ini sepele diimplementasikan dengan overloading operator, misalnya C ++:

#include <iostream>
using namespace std;

class sum {
  int value;
public:
  explicit sum() : sum(0) {}
  explicit sum(int x) : value(x) {}
  sum operator()(int x) const { return sum(value + x); }  // function call overload
  operator int() const { return value; } // integer cast overload
};

int main() {
  int zero = sum();
  cout << "zero sum as int: " << zero << '\n';
  int someSum = sum(1)(2)(4);
  cout << "some sum as int: " << someSum << '\n';
}
amon
sumber
Jawaban yang luar biasa! Kelemahan dengan mengemas varargs dalam daftar adalah Anda kehilangan sebagian aplikasi kari. Saya bermain-main dengan versi Python dari pendekatan terminator Anda, menggunakan argumen kata kunci ..., force=False)untuk memaksa penerapan fungsi awal.
ThomasH
Anda bisa membuat fungsi urutan lebih tinggi Anda sendiri yang sebagian menerapkan fungsi yang mengambil daftar, seperti curryList : ([a] -> b) -> [a] -> [a] -> b, curryList f xs ys = f (xs ++ ys).
Jack
2

Anda mungkin ingin melihat implementasi printf ini di Haskell , bersama dengan deskripsi cara kerjanya . Ada tautan di halaman terakhir ke makalah Oleg Kiselyov tentang melakukan hal semacam ini, yang juga layak dibaca. Bahkan, jika Anda merancang bahasa fungsional, situs web Oleg mungkin harus membaca wajib.

Menurut pendapat saya, pendekatan ini sedikit meretas, tetapi mereka menunjukkan bahwa itu mungkin. Namun, jika bahasa Anda menggunakan pengetikan dengan ketergantungan penuh, itu jauh lebih sederhana. Fungsi variadic untuk menjumlahkan argumen integer-nya kemudian dapat terlihat seperti ini:

type SumType = (t : union{Int,Null}) -> {SumType, if t is Int|
                                         Int,     if t is Null}
sum :: SumType
sum (v : Int) = v + sum
sum (v : Null) = 0

Sebuah abstraksi untuk mendefinisikan tipe rekursif tanpa perlu memberikannya nama eksplisit mungkin membuat penulisan fungsi-fungsi tersebut lebih mudah.

Sunting: tentu saja, saya baru saja membaca pertanyaan itu lagi dan Anda mengatakan bahasa yang diketik secara dinamis , pada saat itu jelas jenis mekanika tidak benar-benar relevan, dan karena itu jawaban @ amon mungkin berisi semua yang Anda butuhkan. Oh well, aku akan meninggalkan ini di sini kalau-kalau ada yang menemukan ini sambil bertanya-tanya bagaimana melakukannya dalam bahasa statis ...

Jules
sumber
0

Berikut ini adalah versi untuk penjelajahan fungsi variadic di Python3 yang menggunakan pendekatan "terminator" dari @amon, dengan mengambil keuntungan dari argumen opsional Python:

def curry_vargs(g):
    actual_args = []
    def f(a, force=False):
        nonlocal actual_args
        actual_args.append(a)
        if force:
            res = g(*actual_args)
            actual_args = []
            return res
        else:
            return f
    return f

def g(*args): return sum(args)
f = curry_vargs(g)
f(1)(2)(3)(4,True) # => 10

Fungsi yang dikembalikan fmengumpulkan argumen yang diteruskan ke dalam panggilan berturut-turut dalam array yang terikat dalam lingkup luar. Hanya ketika forceargumen itu benar, fungsi asli dipanggil dengan semua argumen yang dikumpulkan sejauh ini.

Peringatan implementasi ini adalah bahwa Anda selalu harus melewati argumen pertama fsehingga Anda tidak dapat membuat "thunk", sebuah fungsi di mana semua argumen terikat dan hanya dapat dipanggil dengan daftar argumen kosong (tapi saya pikir ini sejalan dengan implementasi khas kari).

Peringatan lain adalah bahwa sekali Anda melewati argumen yang salah (misalnya dari jenis yang salah) Anda harus kembali kari fungsi aslinya. Tidak ada cara lain untuk mengatur ulang susunan internal, ini hanya dilakukan setelah eksekusi yang sukses dari fungsi curried.

Saya tidak tahu apakah pertanyaan Anda yang disederhanakan, "bisakah objek menjadi fungsi dan nilai non-fungsi pada saat yang sama?", Dapat diimplementasikan dengan Python, sebagai referensi ke fungsi tanpa tanda kurung dievaluasi ke objek fungsi internal . Saya tidak tahu apakah ini bisa dibengkokkan untuk mengembalikan nilai arbitrer.

Mungkin mudah dalam Lisp, karena simbol Lisp dapat memiliki nilai dan nilai fungsi pada saat yang sama; nilai fungsi hanya dipilih ketika simbol muncul di posisi fungsi (sebagai elemen pertama dalam daftar).

ThomasH
sumber