Czy czyste funkcje mogą przyjmować argumenty tylko przez wartość?

2

Przejrzałem internet i obecnie mam następującą definicję czystej funkcji (niezależnie od języka programowania):

  1. Zwracana wartość (jeśli ją ma?) zależy wyłącznie od jej argumentów. — Mówiąc kolokwialnie, nie czyta z obiektu dostępnego z zewnątrz. [wykreślone, bo błędne]
  2. Nie posiada skutków ubocznych (side effects). — Mówiąc kolokwialnie, nie pisze do obiektu dostępnego z zewnątrz. [wykreślone, bo być może błędne]

Jednak zastanawia mnie pewna rzecz: z uwagi na punkt nr 1 wszystkie funkcje, które przyjmują argumenty przez referencję, nie będą czyste.

Dedukując, za czystą można uważać jedynie funkcję przyjmującą argumenty przez wartość.

Dobrze myślę?


UPDATE: Przykład w języku JavaScript:

// Funkcja, która moim zdaniem jest czysta według powyższej definicji
//    - liczby przekazywane są przez wartość *
function(someNumber) {
    return someNumber + 1;
}

// Funkcja, która moim zdaniem nie jest czysta według powyższej definicji
//    - obiekty przekazywane są przez referencję *
function(someObject) {
    return someObject.someProperty;
}

UPDATE2: * W języku JavaScript funkcje generalnie uznawane są za takie, które przyjmują argumenty przez wartość (np. według tej dokumentacji MDN), ale najpewniej nie o takie rozróżnienie mi chodzi. MDN podaje, że "object references are values". Dlatego też pominąłem to rozróżnienie wyżej.

1

Dobra nie za bardzo rozumiem.
Zakładając że mamy takie coś w Javie:


List<String> words = ImmutableList.of("Hello", "There");

//blablabla

public static final int sumOfStringLenght(List<String> strings) {
  return strings.stream()
    .mapToInt(String::length)
    .sum();
 }

Jest to przekazanie wartości referencji, nie wartości prymitywnych(zakładając że o to Ci choddzi), ale na pewno jest to czysta funkcja, bo przecież dla takiej samej listy String będzie zawsze ten sam rezulat

0

@scibi92: nie na pewno jest to czysta funkcja. Lista może się zmienić niezależnie od funkcji, a referencja do niej nie musi. Według powyższej definicji nie jest czysta. Właśnie w moim pytaniu chodzi o to, czy ta definicja jest poprawna. Jeśli nie jest, prosiłbym (w pierwszej kolejności) o podanie jakichś źródeł, które mówią, że funkcje czyste mogą przyjmować argumenty przez referencję.

0

In computer programming, a pure function is a function that has the following properties:[1][2]

Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).

Moim zdaniem jednak jest to czysta jak łza funkcja, dla takiej samej listy zawsze rezultat będzie ten sam, a czytając kolejne wartości nie zmienamy orginalnej listy. Czyli
1)ten sam rezultat dla tej samej listy
2)żadnych efektów ubocznych
Dla mnie wszystko gra i buczy

0

@scibi92: rozumujesz w większości poprawnie, zgodzę się. Ale zauważ, że piszesz: ten sam rezultat dla tej samej listy. Tak by było, gdyby lista nie mogła się zmieniać. Ale może się zmienić, a referencja do niej pozostanie ta sama. W ten sposób dwa wywołania powyższej funkcji przedzielone zmianą listy (nie zmianą referencji do niej) zwrócą inne wartości.


UPDATE: Tak się zastanawiam... w zasadzie nie ma sensu mówić o liście, skoro przekazujemy referencję do niej. Chyba więc odpowiedź na moje pytanie jest: nie muszą tylko przez wartość.

2

funkcja nie zmienia listy wiec jest czysta. to ze probojesz sobie strzelic w kolano to tylko twoja wina ;)

0

Czyli, idąc tropem z mojego poprzedniego postu – wszystkie funkcje przyjmujące argumenty przez referencję (poza obostrzeniami dla funkcji czystych) będą czyste.

1

No masz rację, ale tutaj ale nawet dla mutowalnego argumentu jest to moim zdaniem czysta funkcja. A moim zdaniem mówiąc o argumencie mamy na myśli po prostu wartość, czyli dla refencji to będzie wartośc Seta, Listy, Mapy czy jakiegoś DTOsa - tutaj mamy ta samą wartośc listy. No i poza tym może być referencja do obiektu niemutowalnego ;)

2

@Silv mysle ze takie rozkminy sa wartosciowe, niemniej nie ma co popadac w skrajnosci. mowiac o "czystosci" operujemy na pewnym poziomie abstrakcji, wiadomo ze przy roznych ekstremalnych warunkach (np dzgnieciu srubokretem w cpu) moga wystapic efekty uboczne, ale z punktu widzenia jvm taka funkcja efektow ubocznych nie ma.

1

W językach funkcyjnych listy są często immutable więc jeżeli masz 2 referencje i obie są takie same to masz pewność, że chodzi o listę z tymi samymi bebechami pod spodem. Jeżeli zmodyfikujesz taką listę w jakikolwiek sposób to stworzysz nową referencję więc będziesz miał pewność, że lista już nie jest taka sama. Tym samym wywołanie funkcji z konkretną referencją na listę jako argumentem zawsze zwróci ten sam wynik.

0

Tylko dalej się zastanawiam (@twoj_stary_pijany, niezależnie od tego, co napisałeś): jaki jest sens wyodrębniania takich funkcji czystych? Skoro mogą przyjmować argumenty przez referencję, to w rozumieniu "ogólnym" dwa wywołania funkcji nie będą zwracały takich samych wartości. Dlaczego? Bo jak przekazuję obiekt "Słoń", to nie obchodzi mnie, jaka jest do niego referencja, a raczej jaki jest on sam... czy też referencja także obchodzi? Czyli w ten sposób wyodrębnianie funkcji czystych powinno następować nie na poziomie "obiektowym" (uogólniając pojęcie "obiektu"), tylko na poziomie, hm... mechanizmów danego języka?

0

Nie, funkcji nie obchodzi skąd bierze się argument na którym operuje i wynika to wprost z definicji.

Btw:

Mówiąc kolokwialnie, nie czyta z obiektu dostępnego z zewnątrz.

jest błędne. Może czytać z obiektów zewnetrznych, pod warunkiem, że są niemutowalne (przez konwencję lub konstrukt języka). Inaczej domknięcia (integralny element FP) nie miałyby sensu.

Mówiąc kolokwialnie, nie pisze do obiektu dostępnego z zewnątrz.

To też tylko jeden z wielu potencjalnych efektów ubocznych.

1

Ja nadal stoję przy swoim, jesli przekazujemy Listę Stringów to wartością sa te Stringi a nie referencja. Idąc takim tokiem rozumowania to nie ma w ogóle czystych funkcji, bo w takim Clojure też przekazujesz listy czyli z tego co wiem referencję...

2

Po pierwsze co to jest czysta funkcja, która nie zwraca wartości, to musiałaby dawać jakieś side effects, inaczej byłaby tylko dekoracją w kodzie:) Po drugie primo, chodzi CI chyba o to, że jeżeli Przekazujesz referencję do obiektu, to możesz go zmienić w środku funkcji i wtedy nie byłaby czysta, ale... To zależy w jakim języku Programujesz, jeśli w imperatywnym to praktycznie wszystko Możesz zmienić i zależy od programisty, jak napisze funkcję; natomiast w funkyjnym wszystko jest immutable i nic nie Zmienisz, chyba, że w monadach etc.

0
Silv napisał(a):

Skoro mogą przyjmować argumenty przez referencję, to w rozumieniu "ogólnym" dwa wywołania funkcji nie będą zwracały takich samych wartości.

beda, zakladajac ze te dwa wywolania dostana obiekt o takim samym stanie. przekazywanie np wielkiego pliku xml przez wartosc nie ma zbytnio sensu, wiec musisz sie zadowolic taka a nie inna "czystoscia" ;), ew rzeczywiscie robic tylko niemutowalne rzeczy co generalnie jest utopia.

0

@katelx: No tak, ale stan obiektu jest zależny tylko od tego, czy dany język udostępnia mechanizm do jego zmiany (jeśli udostępnia, nie mamy wpływu na zmianę, np. w jakimś innym module).

0

masz kod w stylu

list.add("test");
int a = calc(list);
list.add("test2");
int b = calc(list);

i teraz jesli a != b to nie wyglada mi to na problem z czystoscia calc... w sumie to nie wiem o co ci chodzi, moglbys dac przyklad pokazujacy ten brak czystosci?

2

Skoro mogą przyjmować argumenty przez referencję, to w rozumieniu "ogólnym" dwa wywołania funkcji nie będą zwracały takich samych wartości.

Ale też nie mają wtedy takich samych argumentów, więc jest wszystko "ok". Może inaczej - funkcja pozostaje czysta, bo przecież funkcja nie operuje na samych referencjach, tylko na obiektach, które się pod nimi kryją (a te są w tym przypadku inne), natomiast kod na zewnątrz funkcji już czysty nie jest wtedy.

Dla funkcji nie ma żadnej różnicy między tymi przykładami:

const fn = (obj) => obj.foo

const a = { foo: 1 }
const result1 = fn(a)
const b = { foo: 2 }
const result2 = fn(b)
const fn = (obj) => obj.foo

const a = { foo: 1 }
const result1 = fn(a)
a.foo = 2
const result2 = fn(a)

Natomiast jako całość pierwszy przykład jest "czysty", drugi nie.

0

@katelx: zakładając wątek, miałem wątpliwość (której wtedy jeszcze nie umiałem wyrazić), czy "czystość" jest zapewniona przez dany język (jego mechanizmy), czy przez jakieś wyższe poziomy abstrakcji. Jeśli przez jakieś wyższe poziomy abstrakcji, to funkcja calc nie byłaby czysta. Jeśli przez język, to by była.

@Maciej Cąderek: nie zgodzę się. Moje rozumienie jest takie, że funkcja operuje na samych referencjach. Nie znam wszystkich mechanizmów C++ czy C, więc mogę się mylić... Co by jednak potwierdzało, że "czystość" zależy wyłącznie od języka.

1

wiekszosc funkcji w wiekszosci jezykow programowania moze w teorii rzucic jakis rodzaj out of memory exception. tak jak wczesniej pisalam - aby zalozyc ze funkcja jest czysta trzeba sobie darowac ekstremalne sytuacje i zalozyc ze:

  • nie ma problemu ze sprzetem czy maszyna wirtualna
  • nie brakuje pamieci
  • inny watek nie manipuluje nam pamiecia
  • nie manipulujemy pamiecia sami (np poprzez jakies callbacki/hooki/hacki)
3

Przy odpowiedniej dyscyplinie (traktowanie obiektów jako niemutowalnych) ew. przy siłowym wymuszeniu niemutowalności (np. Object.freeze) takie coś będzie czyste:

function getFoo(obj) {
   return obj.foo;
}

No bo zakładamy, że obj nie może się zmienić, jest immutable i już. Co z tego, że referencja.

z drugiej strony przy takim kodzie:

const obj = {foo: 123};

function getFoo(obj) {
   return obj.foo;
}
getFoo();
obj.foo++;
getFoo();

to cóż. Czy getFoo będzie czysta?
Może rozbijmy to na dwie części:

  • nie będzie mieć skutków ubocznych. Czyli można ją sobie nawet wywoływać z ileś razy bez ryzyka, że funkcja odpali jakiś skutek uboczny i coś zmieni zewnątrz.
  • ale nie będzie jednak idempotentna. Tzn. jeśli ją wywołamy ileś razy w różnych miejscach, to możemy mieć rózny wynik w zależności od tego co zawiera obj.

Tylko, że punkt drugi może "brudzi" funkcję, ale jednak takie funkcje mogą być bardzo przydatne (np. jako selektory, gettery czy coś, co robi jakieś obliczenia i zwraca wynik).
Nawet bym argumentował, że taka funkcja na jakimś poziomie abstrakcji będzie idempotentna. Bo zawsze będzie zwracać analogiczną wartość (nawet jeśli inną), jej zachowanie/rola będą takie same. To, że obiekt się zmienił pod spodem, to już problem reszty kodu. Funkcja nie jest winna, że coś się zmieniło (zresztą: jeśli obiekt się zmienił, to nie jest to już ten sam obiekt, bo ma inny stan już. Więc obiekt się zmienił w środku, nawet jeśli referencja pozostała taka sama. Więc funkcja dostaje już w argumencie inny (w środku) obiekt, nawet jeśli jest ten sam pod kątem tożsamości. Czyli ma prawo zwrócić inny wynik.

Z drugiej strony taka funkcja:

function foo() {
   return {};
}

zapewne będzie nazwana przez większość czystą funkcją. Ale jednak za każdym razem będzie zwracać inną wartość, bo każdy nowy obiekt jest inny:

console.log(foo() === foo())  // false

i tu pytanie jeszcze o to, czym jest ta sama wartość.

Więc myślę, że generalnie to zależy z której strony się patrzy i do czego nam ta czystość potrzebna (chyba, że z perspektywy formalnej, to przypuszczam, że istnieją jakieś formalne definicje, jednak jestem praktykiem i patrzę od strony praktycznej. Jednak i tak - jak wymyślano programowanie funkcyjne kilkadziesiąt lat temu to nikt i tak nie myślał, że powstanie taki dziwny język jak JavaScript, w którym wszystko jest niepewne).

5

porownywanie referencji nie ma sensu przy ocenianiu czy funkcja jest czysta, powinno sie porownywac stan obiektow wskazywanych przez te referencje... przynajmniej w takiej javie, c++, c#

2

@Silv ależ oczywiście, że @Maciej Cąderek ma rację, to jak przekazujesz argumenty nie ma najmniejszego znaczenia nt. tego czy funkcja jest czysta czy nie. Wszystko zależy od tego jak te dane potem obsługujesz. Przykład w D, gdzie można explicite zaznaczyć, że funkcja ma być czysta:

pure int add(in int a, in int b) { return a + b; }

https://ideone.com/GRqNNe

Funkcja przyjmuje dwie referencje do liczb całkowitych i zwraca nową liczbę. Jak najbardziej czysta funkcja.

2

@Silv: "Co by jednak potwierdzało, że "czystość" zależy wyłącznie od języka" Jednak nie, w każdym (chyba) języku można napisać czystą funkcję (funkcja nie ma side effects i wartość zwracana zależy tylko od argumentów); sytuacja staje się śliska w językach imperatywnych, gdy przyjmujemy referencje do jakiś paskudnie mutowalnych obiektów, ale czystość dalej można osiągnąć jeśli stosujemy się do definicji.

EDIT @LukeJL "tak! :) nawet nie widząc twojego posta, zedytowałem swojego dodając do niego właśnie myśl, że jak zmienimy obiekt poza funkcją, to w rezultacie jest to inny już obiekt (w środku), więc funkcja dostaje inny argument (nawet jeśli o tej samej referencji) więc ma prawo się zachować inaczej..." Właśnie o to chodzi.

1

Moje rozumienie jest takie, że funkcja operuje na samych referencjach.

Nie wiem jak jest w C++, ale w przypadku JSa funkcja nie operuje na referencjach lecz zawsze na wartościach. owszem wartością może być referencja, ale jest to zasadnicza różnica - w JS taki kod nie zmodyfikuje ci zmiennej zewnętrznej:

const fn = obj => {
  obj = { foo: 2 }
}

const x = { foo: 1 }
fn(x)

console.log(x) // => { foo: 1 }
1
lion137 napisał(a):

@Silv: "Co by jednak potwierdzało, że "czystość" zależy wyłącznie od języka" Jednak nie, w każdym (chyba) języku można napisać czystą funkcję (funkcja nie ma side effects i wartość zwracana zależy tylko od argumentów); sytuacja staje się śliska w językach imperatywnych, gdy przyjmujemy referencje do jakiś paskudnie mutowalnych obiektów, ale czystość dalej można osiągnąć jeśli stosujemy się do definicji.

Gorzej jeśli obiekt mutuje się nie tylko się poza funkcją, ale zawiera jakieś gettery, które odpalają paskudne efekty uboczne. Tym sposobem uzyskujesz dostęp do właściwości, myślisz, że nic nie robisz, a się okazuje, że getter coś odpala.

Np. takie elementy DOM. Jeśli się spróbujesz dostać do jakiejś właściwości elementu DOM przez JS, to w rezultacie możesz spowodować kosztowne obliczenia layoutu w przeglądarce (jak to jest opisane na tej liście: https://gist.github.com/paulirish/5d52fb081b3570c81e3a )

1

Btw, @Silv, pytanie - czy ta funkcja jest czysta?

/**
 * @param {number} a
 */
const fn = (a) => a * 2

Bo zgodnie z twoim tokiem rozumowania, wychodzi chyba na to, że nie.

Edit:

Tłumaczę dlaczego ta funkcja jest tak samo "problematyczna", jak funkcja przyjmująca wartość referencji do obiektu:

Weźmy takie wywołania funkcji:

/**
 * @param {number} a
 */
const fn = (a) => a * 2

let a = 1
const result1 = fn(a)
a = 2
const result2 = fn(a)

console.log(result1, result2)

Jaka jest różnica jakościowa między poprzednim przykładem a tym:

/**
 * @param {Object} a
 */
const fn = (a) => a.foo * 2

let a = {foo: 1}
const result1 = fn(a)
a.foo = 2
const result2 = fn(a)

console.log(result1, result2)

W obu wywołanie dwa razy z tą samą zmienną da różny rezultat.
To, że w drugim przykładzie mamy gdzieś po drodze referencję, jest przezroczyste dla funkcji (o ile nie próbujemy w funkcji modyfikować obiektu).

3

"W obu wywołanie dwa razy z tą samą zmienną da różny rezultat" Przecież nie Wywołałeś z tym samym argumentem. Za pierwszym razem Wywołałeś funkcję na obiekcie a. którego parametr foo miał wartość 1, a potem na a, którego foo wynosiło dwa; czyli Wywołałeś na dwu różnych argumentach.

0

@lion137:

Przecież nie Wywołałeś z tym samym argumentem.

Bingo. Ta sama zmienna !== ten sam argument. Dokładnie to co opisałem wcześniej.

2
Silv napisał(a):

Przejrzałem internet i obecnie mam następującą definicję czystej funkcji (niezależnie od języka programowania):

  1. Zwracana wartość (jeśli ją ma?) zależy wyłącznie od jej argumentów. — Mówiąc kolokwialnie, nie czyta z obiektu dostępnego z zewnątrz.
  2. Nie posiada skutków ubocznych (side effects). — Mówiąc kolokwialnie, nie pisze do obiektu dostępnego z zewnątrz.

Jednak zastanawia mnie pewna rzecz: z uwagi na punkt nr 1 wszystkie funkcje, które przyjmują argumenty przez referencję, nie będą czyste.

Coś ci nie wyszło tłumaczenie :) Zajrzyjmy do Wiki https://en.wikipedia.org/wiki/Pure_function :

In computer programming, a pure function is a function that has the following properties:[1][2]

  • Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
  • Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).

A więc stwierdzenie nie czyta z obiektu dostępnego z zewnątrz jest błędne. Poprawne stwierdzenie to - dla tych samych argumentów (włączając przypadek z brakiem argumentów) dwa wywołania mają zwrócić to samo. Jeżeli argumenty są różne to dwa wywołania mogą (ale nie muszą) zwrócić różne wartości. Czysta funkcja może odczytywać i często odczytuje stan, który nie jest przekazany jej przez argumenty. Przede wszystkim czysta funkcja może czytać do woli z globalnego niemutowalnego stanu (ale może się też domykać na lokalnym niemutowalnym stanie).

1 użytkowników online, w tym zalogowanych: 0, gości: 1