Wątek przeniesiony 2022-12-16 09:55 z Nietuzinkowe tematy przez Riddle.

Czy stosowanie pętli for to obciach?

0

Załóżmy, że mam tablicę i chcę wykonać pewne operacje na niej. Czyli muszę przeiterować po każdym elemencie.
Można to zrobić na kilka sposobów. Klasyczną pętlą for, albo map() lub forEach().

Spotkałem się ze stwierdzeniem, że for stosuje się "w przedszkolu". Natomiast "profesjonalne" podejście to kod typu myArray.filter().map()...

Co o tym sądzicie?

4

Nie wiem jak w innych języka ale w C# z tego co pamiętam to kompilator generuje taki sam kod wynikowy zarówno dla for jak i foreach więc w C# różnicy w wydajności bym się nie spodziewał, ale może ktoś potwierdzi/zaprzeczy. W innych językach może być inaczej.

Ja używam foreach gdzie się da, zwłaszcza, że dużo operuję na kolekcjach, bo jest dla mnie czytelniejszy niż for a jeśli chodzi o lukier składniowy to najbardziej w C# lubię LINQ i jego ForEach, zamiast tradycyjnego foreacha, ale nie w każdej sytuacji da się go użyć bez rzutowania na listę.

2

Wszystko zależy od języka, każde narzędzie przydaje się do czegoś innego. Jak robisz operacje na wszystkich elementach to robisz foreach. Jak na części to jakiś filtr a potem foreach. Jak piszesz jakiś algorytm i musisz robić przy okazji jakieś magiczne ruchy to for. Nie uważam aby stosowanie for było błędem :d ale po to są takie narzedzia jako foreach aby kod był po prostu czytelniejszy.

0

Chcę przeiterować elementy od końca. Wydaje się, że for będzie najlepszy:

  • mogę deklarować krok
  • mogę deklarować od którego elementu
  • mogę deklarować kierunek

Inaczej potrzebne są jakieś kombinacje typu (używając JS) myString.split().reverse().join()

2
markone_dev napisał(a):

Nie wiem jak w innych języka ale w C# z tego co pamiętam to kompilator generuje taki sam kod wynikowy zarówno dla for jak i foreach więc w C# różnicy w wydajności bym się nie spodziewał, ale może ktoś potwierdzi/zaprzeczy. W innych językach może być inaczej.

7

Zależy od języka i preferencji danego programisty. Oraz jaki mamy cel, po co chcemy iterować.

W JS:
jeśli chcę przejechać przez elementy tablicy, to robię zwykle .forEach:

someArray.forEach(el => { console.log(el); });

jest to bardziej "skrótowe" niż pętla for i niektórzy się tym zachwycili i zaczęli rzucać teksty "nie używam już pętli for".

Tylko, że to krótkowzroczne myślenie. Pętla for pozwala na większą elastyczność. Np. iterację od tyłu (przydatne przy usuwaniu elementów). Albo dowolną iterację, nie tylko po tablicach. Również za pomocą for można przerwać iterację w trakcie, co też bywa zaletą. Np. jeśli element znaleziony, to nie musisz dalej przeszukiwać...

...ale z kolei do szukania elementów dodano metody find, findIndex , findLast, findLastIndex.

Dalej. Jeśli chcemy zamienić jedną tablicę w drugą, to mamy od tego map, które całkiem wygodnie pozwala na takie rzeczy.

myArray.filter().map()...

Taki kod również jest przydatny - najpierw przefiltrować tablicę i potem przemapować.
(jednak to, że tak można zrobić w pewnych przypadkach nie kasuje przydatności pętli for czy innych sposobów iteracji).

Dalej mamy reduce, które też jest przydatne jeśli chcemy zrobić z wielu elementów jeden (np. policzyć sumę elementów).
Natomiast ponieważ reduce jest dość elastycznym sposobem iteracji, to ludzie zaczęli tego nadużywać. W szczególności zobaczysz kody, które mutują pierwszy argument - np. dodają coś do obiektu, to częsty pattern:

 someArray.reduce((acc, v, i) => { acc[v] = i; return acc }, {})

chociaż to hak i używanie czegoś niezgodnie z przeznaczeniem (bo reduce kojarzy się z programowaniem funkcyjnym, więc mutacja trochę nie bardzo pasuje). Chociaż nie jestem radykałem, sam tak robiłem. Nawiasem mówiąc powyższy przykład z reduce można i tak zapisać krócej używając Object.fromEntries:

 Object.fromEntries(someArray.map((v,i) => [v,i]))

Jeśli chcemy utworzyć obiekt na podstawie tablicy.

W innych przypadkach użycie reduce można przepisać np. do zwykłej pętli for czy użycia metody .forEach. I często jest to bardziej czytelne (reduce ma dziwną skłonność zaciemniania kodu, bo często ludzie tam wrzucają imperatywny kod, ale pisany tak, żeby imitować programowanie funkcyjne).

Więc jeśli już ograniczyć się w używaniu czegoś, to sugerowałbym ostrożność w użyciu reduce ;) można oczywiście używać, ale łatwo napisać coś mało czytelnego używając reduce.

Długi post wyszedł, ale i tak tylko liznąłem temat. Bo poruszyłem tylko iterację po tablicach (a można jeszcze iterować po obiektach, iterablach itp.) oraz przez pętlę for mam na myśli for (init; cond; after) a w JS jest jeszcze pętla for... in, for... of czy for await...of.

Anyway, nie daj sobie wmówić głupot, że pętla for jest be. Wszystko zależy co robisz.

No i jeszcze są pętle while czy do while, które też są przydatne, chociaż rzadziej (ale np. przy implementacji różnych algorytmów mogą się przydać).

4

Dodam, pętla (foreach / for) w stosunku do streamów / lambd jest perfekcyjnie podatna do śledzenia debugerem (Java / C#)

Nie zawsze autor jest w najwyższej kondycji intelektualnej, nie zawsze dane są czyste, czasem tropimy jakiś wyjątek po 2 latach pozytywnej eksploatacji, okazuje się że święte założenia co do danych nie są takie święte itd...

1

W pythonie mają te funkcje duże różnice, ze względu na to jak język działa w wirtualnej maszynie, funkcje i zmienne są wyszukiwane jako stringi i możliwe są pewne optymalizacje, w niskopoziomowych językach nie ma to znaczenia bo każdy kod może być praktycznie tak samo przedstawiony, ale w dynamicznych może w internalsach być spora różnica.

2

Kod ma być najprostszy i czytelny tak, żeby zrozumiał go stażysta.
Tak więc żenujące jest robić fikołki tylko po to, żeby ładnie wyglądało

6
Czitels napisał(a):

Kod ma być najprostszy i czytelny tak, żeby zrozumiał go stażysta.
Tak więc żenujące jest robić fikołki tylko po to, żeby ładnie wyglądało

Trochę się zgadzam, trochę nie.
Tj. zgadzam się, żeby pisać czytelny kod i że bezsensu jest robić fikołki tylko po to, żeby się popisać jakąś techniką.

Jednak z tak, żeby zrozumiał go stażysta mam problem. Jeśli będziemy się dostosowywać do stażystów, to okaże się, że niewiele się da zrobić i będziemy ogłupiać własny kod i np. nie używać zaawansowanych ficzerów w języku czy pewnych wzorców/technik, tylko dlatego, że nowicjusz może tego nie zrozumieć. A przecież to odwrotnie powinno być - stażysta wchodząc do projektu powinien się uczyć nowych technik i rozwijać, pytać o coś, jak nie rozumie itp.

2

Kod ma być najprostszy i czytelny tak, żeby zrozumiał go stażysta.

W sensie ze filter, map jest nieczytelne?

1

Jeżeli ktoś zna tylko for czy while to taki map() z operatorem lambda wywoła tylko jeden efekt: WTF.

5
kosmonauta80 napisał(a):

Jeżeli ktoś zna tylko for czy while to taki map() z operatorem lambda wywoła tylko jeden efekt: WTF.

Jak ktoś nie umie programować to wiele rzeczy będzie dla niego wtf-em.

Dla mnie sprawa jest prosta (z perspektywy C#):

  1. Mutowanie lub side-effecty - foreach. Nigdy nie napisałem ani na szczęście nie widzę używania ForEach w tym kontekście, jakiś wygibasów z in-house extension methodami, bo IEnumerable nie ma ForEach.
  2. Select (czy jak kto woli map) w przeciwnym wypadku. Lambdy i LINQ są mocno zakorzenione w .NET i nikt nie marudzi, że to przejściowa moda na funkcyjne i for jest bardziej naturalny.

while to ma sens chyba tylko przy while(true), a for to w sumie tylko jak mamy mutowanie/side-effecty i potrzebujemy dostęp do indeksu.

6
kosmonauta80 napisał(a):

Jeżeli ktoś zna tylko for czy while to taki map() z operatorem lambda wywoła tylko jeden efekt: WTF.

U Ciebie wszystko wywoluje WTFa ;) tak zauwazylem

3

Oczywiście że for to siara. Prawdziwy programista pisze LDR PC, <adres instrukcji>.

3

Chciałem napisać że w Haskellu to nawet nie ma fora, ale wcześniej sprawdziłem w Hoogle i nawet jest :P Zaimplementowany jako traverse z argumentami w odwrotnej koleności :D

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)
for = flip traverse

XD

0

Biorąc pod uwagę, że javowe Stream API jest bardzo okrojone jeśli chodzi o funkcjonalność względem for / while to właśnie streamy są dla przedszkolaków ;)
Główna siła streamów polega na tym, że kiedy do nich przywykniesz to są bardziej czytelne. Natomiast jeśli chodzi o wydajność to leżą i kwiczą, a jeśli do tego dorzucisz, że bardziej skomplikowane operacje na streamach robią się średnio czytelne to jest jeszcze słabiej.

0

@rad1317: to, że ktoś może pomylić "streamy są wolniejsze od pętli" z "należy unikać streamów" to już nie mój problem. Równie dobrze mógłbym napisać "jeśli nie napiszesz, że streamy są wolniejsze od pętli to potem znajdzie się ktoś, kto to przeczyta i będzie wrzucał streamy nie tam gdzie trzeba".

Jeśli chodzi o czytelność - to przestrzegam cię po prostu przed zwrotami w stylu "pętle są zawsze mało czytelne" - bo w programowaniu naprawdę trudno znaleźć takie ostateczne prawdy (swoją drogą całkiem niedawno dyskutowałem o tym, że zapis z streamami jest dla mnie czytelniejszy niż taki bardziej imperatywny). Taki bardzo prosty przykład:

for (Request request: requests) {
   for (Element element: request.elements()) {
      for (Attribute attribute: element.attributes()) {
         if ("someName".equals(attribute.getName())) {
            doSomething(attribute.getName());
         }
      }
   }
}

// Na streamach jest to dużo prostsze: 

requests.stream()
   .flatMap(r -> r.elements().stream())
   .flatMap(e -> e.attributes().stream())
   .map(a -> a.getName())
   .filter(name -> "someName".equals(name))
   .forEach(this::doSomething);

Proste, prawda? No to lekka modyfikacja:

for (Request request: requests) {
   for (Element element: request.elements()) {
      for (Attribute attribute: element.attributes()) {
         if ("someName".equals(attribute.getName())) {
            doSomething(request, element, attribute);

            break;
         } else if ("otherName".equals(element.getName())) {
            break;
         }
      }
   }
}

No i teraz pytanie - jak to zapiszesz? Tak, żeby nie trzeba było zbytnio skakać oczami pomiędzy kilkoma kawałkami kodu.

2

W Scali dosyc prosto.


.collectFirst {
  case "someName" => doSmth()
  case "otherName" => ()
}

Na JS-owym Javovym API sie nie znam. Moze tam nie ma takich funkcji.

EDIT:

A dobra. Bo tutaj sie jeszcze wywolanie doSomething(request, element, attribute); zmienilo. No to troche wiecej trzeba

 (for {
   request <- requests
   element <- request.elements
   attribute <- element.attributes
 } yield (request, element, attribute))
   .collectFirst {
     case (request, element, attribute) if attribute.name == "someName" => doSomething(request, element, attribute)
     case (_, _, attribute) if attribute.name == "otherName" => ()
   }

* for-comprehension to lukier skladniowy na flatMapy, nie jest to petla per se. Jakby ktos sie mial czepiac ;)


EDIT2: Co nie znaczy, ze nie zgadzam sie z haslem it depends

1

No akurat, że w językach mocno funkcyjnych takie rzeczy robi się łatwiej, to wiem. Sam pisałem kiedyś wstawki w Q z KDB i tam przekazywanie nadrzędnego elementu jest jeszcze prostsze niż w Scali. Mi głównie chodziło o javowe Streams API, bo od dłuższego czasu widzę dziwną kargokultyzację tego rozwiązania, tzn. ludzie tworzą niespotykanie skomplikowane streamy,

W Streams API problemem jest to, że nie masz kluczowego słówka yield, co oznacza tyle, że musisz się gimnastykować. Tj.

// Takie coś
for (Request request: requests) {
   for (Element element: request.elements()) {
      doSomething(request, element);
   }
}

// Trzeba zapisać jako takie coś:
requests.stream()
   .flatMap(r -> r.elements().stream().map(e -> Tuple2.of(r, e)))
   .forEach(t -> doSomething(t.f0, t.f1));

// Albo tak:
Function<Request, Stream<Tuple2<Request, Element>> flattenElements = (r -> r.elements().stream().map(e -> Tuple2.of(r, e)));
requests.stream()
   .flatMap(flattenElements)
   .forEach(t -> doSomething(t.f0, t.f1));

// Albo pójść na prosto, tj.
Consumer<Request> consume = (r -> r.elements().stream().forEach(e -> doSomething(r, e)));
requests.stream().forEach(consume);

Wtedy tracisz podstawową zaletę javowych streamów - tj. prostolinijne czytanie od dołu do góry, no i przy użyciu tupli musisz sprawdzać, co leży pod t.f0, a co pod t.f1. Już lepiej faktycznie lecieć po skosie, ewentualnie wykorzystać vavr.io, chociaż to ostatnie to ciągle jest samo API.

0

Dla mnie zarówno pętla for (i każda inna) jak i java.util. streamy to obciach (java i kotlin). Z tym, że java.util nawet większy - bo poletko zastosowań jest nikłe.

0

Btw. Wydaje mi sie, ze w przypadku OP-a chodzi raczej o JS a nie stream-api

1

Jeśli celem jest produkowanie wartości wówczas map/filter jest lepsze, bo taki kod można łatwiej:

  1. przekształcić, by wykonać równolegle
  2. wartościować/ewaluować leniwie
  3. oraz ostatecznie postrzegać jako wyrażenie, które nie tylko łatwo rozbudować, ale również zapisać w repl.

Natomiast jeśli celem jest mutowanie stanu czy wywoływanie innych efektów to wtedy for jest lepszy, bo taki kod łatwiej jest debugować, oprzeć na przerwaniach, łączyć z wyjątkami. Oczywiście warto ograniczyć mutowanie, ale jeśli program nie ma efektów to raczej wątpliwe ma zastosowanie.

Dla mnie obciachem jest sytuacja, gdy osoba jest zaślepiona składnią, opiera się wyłącznie ku jednej stronie nie biorąc pod uwagę celu do jakiego musi w danej sytuacji dążyć, by rozwiązać problem. Jeśli ktoś próbuje np. robić efekty, proszę bardzo, ale lewniwe wartościowanie i efekty to wówczas proszenie się o dodatkowe problemy.

EDIT:
Zasadniczo cały wątek sprowadza się do pytania co lepsze: instrukcje czy wyrażenia? Na ogół jedno bez drugiego nie ma większego sensu.

3
kosmonauta80 napisał(a):

Spotkałem się ze stwierdzeniem, że for stosuje się "w przedszkolu". Natomiast "profesjonalne" podejście to kod typu myArray.filter().map()...

Co o tym sądzicie?

Jak najbardziej. Nierozsądnie jest używać do przeszukiwania/filtrowania/mapowania danych z kolekcji jakichś rozwlekłych pętli, jeśli mamy biblioteki pozwalające pisać nam bardziej zwięzły kod.

1

Czemu miałbym używać pętli for, gdy istnieje bardziej czytelny i zwięzły zapis? To, że ktoś nie rozumie, jak działa map to chyba tylko jego problem. To niech się douczy.

1

Obciachem jest mówienie że to obciach.

2

For tak samo dobry jak map. Zależy od zastosowania. Równie często używam for i while, foreach - rzeczywiście for używam najrzadziej ale używam.
Jak kogoś boli używanie pętli for czy while to moim zdaniem coś jest z nim "nie tak".
Pisanie, że for to obciach to owszem obciach ale dla tego co tak pisze...
Ogólnie dyskusja na tym samym poziomie co "taby czy spacje".

1

Najlepiej dobrać narzędzia do problemu, znając również wady i zalety poszczególnych narzędzi w danym kontekście i języku programowania (bo i od języka programowania to będzie zależeć, czego się najlepiej używa).

pętli for

pętle for też bywają różne. W wielu językach do iteracji po kolekcji można zrobić coś w stylu for a in b i to jest bardziej czytelne niż for (init; cond; after). Do Pythona nawet pętle for wsadzili do list comprehension i ludzie z tego korzystają i uważają za cool.

Przykład Pythona. Powiedzmy, że mamy listę liczb:

numbers = [1, 2, 3, 4, 5, 6]

i chcemy przefiltrować tylko parzyste [2, 4, 6] i pomnożyć przez dwa, czyli uzyskać [4, 8, 12].

możemy to zrobić tak (list comprehension)

out = [a * 2 for a in numbers if a % 2 == 0]

albo tak (używając pętli for):

out = []
for a in numbers:
	if a % 2 == 0:
		out.append(a * 2)

albo używając map i filter:

out = list(map(lambda a: a * 2, filter(lambda a: a % 2 == 0, numbers)))

No i moim zdaniem wersja z map i filter jest najmniej czytelna z tych opcji.

0

Nie.

0

Ja powiem, że nie mam takiej wiedzy, mam w C++ std::ranges::any_of i ona iteruje i sprawdza nie wiem czy przerwie się przy pierwszym znalezionym czy nie i może to czasem opóźnić wszystko.

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