Cookies op Tweakers

Tweakers maakt gebruik van cookies, onder andere om de website te analyseren, het gebruiksgemak te vergroten en advertenties te tonen. Door gebruik te maken van deze website, of door op 'Ga verder' te klikken, geef je toestemming voor het gebruik van cookies. Wil je meer informatie over cookies en hoe ze worden gebruikt, bekijk dan ons cookiebeleid.

Meer informatie
Toon posts:

[Javascript] Van imperatief naar functioneel

Pagina: 1
Acties:

Vraag


  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
Ik ben een vrij begaafd imperatief (javascript) programmeur. Alleen heb ik de ontwikkelingen sinds ES6 een beetje gemist. Met name de functionaliteit die het nu mogelijk maakt om functioneel te programmeren. Functioneel programmeren is een vaardigheid die ik al langer zou willen ontwikkelen en nu leek het me leuk om dit voor een nieuw javascript project eens zuiver te gaan proberen.

Als eerste probeer ik een string transformatie te doen. Ik vraag me alleen af of ik dit functioneel zuiver genoeg heb gedaan. Oftewel, kunnen jullie een code review doen en mij tips en/of trucs geven? Die code review hoeft zich niet te beperken tot alleen het functionele gedeelte, maar mag zich op alles richten waar jullie denken dat het beter kan.


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const data = "id: 37\nevent: bar\ndata: a\n\nid: 38\nevent: foo\ndata: b\n\n";
const obj = data
    .split("\n\n")
    .slice(0, -1)
    .map(_ =>
        _
            .split('\n')
            .map(_ => [ _.split(':')[0], _.split(':')[1] ])
    )
    .reduce(
        (a, b) => (
            a.concat(
                b.reduce((x, y) => {
                    Object.assign(x, { [y[0]]: y[1].trim() });
                    return x;
                }, {})
            )
        ), []
    );

console.log(JSON.stringify(obj));



Dit stukje moet dit JSON object opleven:

JavaScript:
1
2
3
4
5
6
7
8
9
[{
    "id": "37",
    "event": "bar",
    "data": "a"
}, {
    "id": "38",
    "event": "foo",
    "data": "b"
}]

geen vragen via PM die ook op het forum gesteld kunnen worden.

Beste antwoord (via CurlyMo op 07-01-2020 19:48)


  • R4gnax
  • Registratie: maart 2009
  • Laatst online: 17:43
CurlyMo schreef op vrijdag 3 januari 2020 @ 21:46:
mag zich op alles richten waar jullie denken dat het beter kan
Paar puntjes:
  1. Parameters die je gebruikt noem je niet _. Die naam is gereserveerd voor een discard (d.w.z. ongebruikte) parameter.
  2. Je bent een array reduce operatie aan het doen die non-conditioneel concateneert aan een accumulator wat ook een array is. Dat is dus gewoon een array map operatie, maar dan super inefficient.
  3. Je weet blijkbaar nog niet dat string.split een regex als argument kan nemen en dat capturing groups in het gesplitte array terecht komen. Dit kun je goed gebruiken om een string te splitsen 'op het eerste voorkomen van X.'
  4. Destructuring en spreading maakt e.e.a. een stuk beter leesbaar.
Kijk bijv. hier naar:


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const data = "id: 37\nevent: bar\ndata: a\n\nid: 38\nevent: foo\ndata: b\n\n";

const obj = data
  .trim()
  .split( "\n\n" )
  .map( item => item
    .trim()
    .split( "\n" )
    .reduce(( acc, prop ) => {
      const [ key, val ] = prop.split( /\s*:\s*(.+)/ );
      return { ...acc, [ key ] : val };                                                 
     }, {})
  );

console.log( JSON.stringify( obj, null, "  " ));

Alle reacties


  • WernerL
  • Registratie: december 2006
  • Laatst online: 21:26
Niet de meest efficiente manier om een string te parsen, maar het is in ieder geval een pure functie. :+
Als je de onderliggende concepten wil leren begrijpen verdiep je eens in category theory. Dat is waar het hele paradigma op is gebasseerd. Functioneel programmeren is meer dan immutable functies en functie-compositie maar de basis lijk je in ieder geval onder de knie te hebben. :)

Volgende stap, parser combinators? :D https://github.com/gcanti/parser-ts

  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
WernerL schreef op vrijdag 3 januari 2020 @ 21:56:
Niet de meest efficiente manier om een string te parsen, maar het is in ieder geval een pure functie. :+
Volgens mij leidt dit snel naar een hele andere discussie :p

Wel fijn om te horen dat het puur is.

geen vragen via PM die ook op het forum gesteld kunnen worden.


  • Ed Vertijsment
  • Registratie: juli 2014
  • Laatst online: 21:24
Is er iets mis, of niet functioneel aan om named functies te gebruiken en die als callbacks mee te geven? Dan is het imo beter leesbaar wat er nu eigenlijk gebeurt.

Verder heb je nu aardig wat loops alleen voor je string. Kan vast efficiënter maar weet niet of dat hier het doel is.

svenv.nl


  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
Ed Vertijsment schreef op zaterdag 4 januari 2020 @ 14:27:
Kan vast efficiënter maar weet niet of dat hier het doel is.
Tuurlijk, met de 'beperking' dat het wel functioneel puur moet blijven.

geen vragen via PM die ook op het forum gesteld kunnen worden.


Acties:
  • +1Henk 'm!

  • BarôZZa
  • Registratie: januari 2003
  • Laatst online: 02:07
@CurlyMo Eerlijk gezegd vind ik dit vooral nog een imperatieve gedacchtengang die is omgezet naar wat functies.

Wat je eigenlijk doet is gewoon een berg instructies geven die in een bepaalde volgorde moeten worden uitgevoerd.

Dus: Split eerst op \n\n, doe dan een slice, dan een split op \n, dan een reduce etc etc.

Dat lukt nog bij bij deze eenvoudige opdracht, maar als je bijvoorbeeld de syntax zou willen uitbreiden, dan wordt het al heel lastig omdat je dan niet weet of je als eerst kan splitten op \n\n omdat er bijvoorbeeld ook \r\r kan staan. Doortdat in de huidige code alles zo aan elkaar is geknoopt, kan je er ook niet vanaf wijken. Laat je een : weg dan crasht de functie.
Daarnaast moet nu de gehele dataset in het geheugen zijn geladen voordat je kan beginnen met parsen. Bij grote datasets is dat niet werkbaar.

Het is verstandiger om simpelweg van links naar rechts één voor één alle karakters te parsen(data.split('')) en vervolgens controleer je of het teken een : of \n is of iets anders en hang je daar een actie aan. Zo kan je de parser simpelweg uitbreiden door nieuwe functies toe te voegen.

  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
BarôZZa schreef op zondag 5 januari 2020 @ 07:47:
@CurlyMo Eerlijk gezegd vind ik dit vooral nog een imperatieve gedacchtengang die is omgezet naar wat functies.
Je hebt me door :)
Het is verstandiger om simpelweg van links naar rechts één voor één alle karakters te parsen(data.split('')) en vervolgens controleer je of het teken een : of \n is of iets anders en hang je daar een actie aan. Zo kan je de parser simpelweg uitbreiden door nieuwe functies toe te voegen.
Dan kom je al snel bij een recursive decent parser uit. Heel leuk om te maken, maar volgens mij niet iets wat specifiek functioneel is en vrij overkill voor deze situatie IMHO. Mijn recursive decent descent parser in C is gewoon volledig imperatief.

geen vragen via PM die ook op het forum gesteld kunnen worden.


  • Sandor_Clegane
  • Registratie: januari 2012
  • Laatst online: 16:22

Sandor_Clegane

Fancy plans and pants to match

Volgens mij wil je altijd een decent parser........

Anders heeft het weinig nut, a great parser zou nog mooier zijn.

Sandor_Clegane wijzigde deze reactie 06-01-2020 07:14 (40%)


Acties:
  • Beste antwoord
  • +1Henk 'm!

  • R4gnax
  • Registratie: maart 2009
  • Laatst online: 17:43
CurlyMo schreef op vrijdag 3 januari 2020 @ 21:46:
mag zich op alles richten waar jullie denken dat het beter kan
Paar puntjes:
  1. Parameters die je gebruikt noem je niet _. Die naam is gereserveerd voor een discard (d.w.z. ongebruikte) parameter.
  2. Je bent een array reduce operatie aan het doen die non-conditioneel concateneert aan een accumulator wat ook een array is. Dat is dus gewoon een array map operatie, maar dan super inefficient.
  3. Je weet blijkbaar nog niet dat string.split een regex als argument kan nemen en dat capturing groups in het gesplitte array terecht komen. Dit kun je goed gebruiken om een string te splitsen 'op het eerste voorkomen van X.'
  4. Destructuring en spreading maakt e.e.a. een stuk beter leesbaar.
Kijk bijv. hier naar:


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const data = "id: 37\nevent: bar\ndata: a\n\nid: 38\nevent: foo\ndata: b\n\n";

const obj = data
  .trim()
  .split( "\n\n" )
  .map( item => item
    .trim()
    .split( "\n" )
    .reduce(( acc, prop ) => {
      const [ key, val ] = prop.split( /\s*:\s*(.+)/ );
      return { ...acc, [ key ] : val };                                                 
     }, {})
  );

console.log( JSON.stringify( obj, null, "  " ));


  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
R4gnax schreef op maandag 6 januari 2020 @ 22:02:
[...]


Paar puntjes:
• Parameters die je gebruikt noem je niet _. Die naam is gereserveerd voor een discard (d.w.z. ongebruikte) parameter.
Check!
• Je bent een array reduce operatie aan het doen die non-conditioneel concateneert aan een accumulator wat ook een array is. Dat is dus gewoon een array map operatie, maar dan super inefficient.
Ik was aan het stoeien hoe ik de array kon vullen met objecten. Dat was waarom ik op een gegeven moment reduce ben gaan stapelen. Dat terwijl map op zichzelf al een array vult. Stom van me :)
• Je weet blijkbaar nog niet dat string.split een regex als argument kan nemen en dat capturing groups in het gesplitte array terecht komen. Dit kun je goed gebruiken om een string te splitsen 'op het eerste voorkomen van X.'
• Destructuring en spreading maakt e.e.a. een stuk beter leesbaar.
Klopt, maar dit is niet per se een essentiële verbetering, maar zeker wel leerzaam.

Als ik jouw verbeteringen in mijn eigen woorden had toegepast (dus als ik je voorgezegde antwoord niet had gehad, maar alleen jouw commentaar had gelezen), dan had ik het zo gedaan.


JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
data
    .split("\n\n")
    .slice(0, -1)
    .map(item => 
        item
            .split('\n')
            .reduce(( a, b ) => {
                const arr = b.split(":");
                return Object.assign(a, { [ arr[0] ] : arr[1].trim() }); 
            }, {})
        )
    );


De doorslaggevende hint was voor mij in mijn eigen woorden:
- Een map bouwt een array op, dus je hebt geen gestapelde reduce nodig om datzelfde inefficiënt te bereiken.

Voordeel van jouw regex is dat je de slice niet nodig hebt.

Wat vind je verder van de feedback van @BarôZZa dat het nog steeds te imperatief gedacht is?

geen vragen via PM die ook op het forum gesteld kunnen worden.


  • R4gnax
  • Registratie: maart 2009
  • Laatst online: 17:43
CurlyMo schreef op maandag 6 januari 2020 @ 22:26:
Wat vind je verder van de feedback van @BarôZZa dat het nog steeds te imperatief gedacht is?
Ik denk dat @BarôZZa daar de plank een beetje misslaat.
Of tenminste; twee dingen door elkaar aan het halen is.

Dat je een recept in stap 1,2,3 volgt wil niet meteen zeggen dat het niet functional is. Puur functional gezien zou de notatie 3(2(1())) worden, of op een andere manier gecomposed, maar alsnog loop je door die stappen heen.

JavaScript kan functional dingen doen, maar de notatie ervan blijft altijd een beetje in het imperatieve steken omdat je met member methods te maken hebt. Je kunt het hee----l omslachtig om gaan schrijven door met allerlei helper functies de map, reduce, etc. operaties los trekken van Array.prototype en het geheel point-free te maken, maar wat brengt je dat behalve een bende extra werk en minder leesbaarheid?

Verder is het inderdaad wel zo dat de huidige code niet heel erg robuust is mbt parsing fouten, en ook niet erg makkelijk uitbreidbaar is omdat je een 1-2-3 recept volgt. Maar nogmaals: dat zou je in pure functional stijl ook kunnen doen met een 'simpel' geval.

De echte oplossing daarvoor is het genereren van een parser adhv een standaard grammatica in bijv. BNF. En dat is noch imperative noch functional, maar is in de kern declarative.

Het samenstellen van die grammatica kan je dan weer wel op imperative evenals op functional wijze doen. Parser combinators met een library als parser-ts is inderdaad zoiets in laatstgenoemde categorie.


Waar jij trouwens wel nog echt de fout in gaat is met je gebruik van Object.assign. Dat is namelijk state mutatie en dat mag niet. Zeker niet als deze ook nog buiten de scope van een lokale functie ontsnapt. ;)

R4gnax wijzigde deze reactie 06-01-2020 23:16 (6%)


  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
R4gnax schreef op maandag 6 januari 2020 @ 23:09:
[...]
De echte oplossing daarvoor is het genereren van een parser adhv een standaard grammatica in bijv. BNF. En dat is noch imperative noch functional, maar is in de kern declarative.
Het maken van parser ben ik bekend mee. Al vaker gedaan, maar de keuze dat niet te doen is een simpele kosten baten. In dit geval vond ik dit veel te lomp voor een simpele string waar ik ook nog eens volledige controle over heb.
Waar jij trouwens wel nog echt de fout in gaat is met je gebruik van Object.assign. Dat is namelijk state mutatie en dat mag niet. Zeker niet als deze ook nog buiten de scope van een lokale functie ontsnapt. ;)
Ik snap hem. Had ik die dan wel zo mogen gebruiken?

JavaScript:
1
return Object.assign({}, a, { [ arr[0] ] : arr[1].trim() });

geen vragen via PM die ook op het forum gesteld kunnen worden.


  • Sandor_Clegane
  • Registratie: januari 2012
  • Laatst online: 16:22

Sandor_Clegane

Fancy plans and pants to match

Je zou eens kunnen kijken naar Elm json parsers.

  • R4gnax
  • Registratie: maart 2009
  • Laatst online: 17:43
CurlyMo schreef op dinsdag 7 januari 2020 @ 08:07:
Ik snap hem. Had ik die dan wel zo mogen gebruiken?
JavaScript:
1
return Object.assign({}, a, { [ arr[0] ] : arr[1].trim() });
Yup. Het muteert weliswaar nog steeds die nieuwe object literal, maar dat maakt niet uit, want die is speciaal voor die operatie aangemaakt en dat blijft controleerbaar.

Er staat dan eigenlijk hetzelfde als

JavaScript:
1
return { ...a, [ arr[ 0 ]] : arr[ 1 ].trim() };


En met wat destructuring er bij, wordt dat:

JavaScript:
1
2
const [ key, value ] = arr;
return { ...a, [ key ] : value.trim() };


Wat er dan weer redelijk bekend uit zou moeten zien. ;)

  • CurlyMo
  • Registratie: februari 2011
  • Laatst online: 23:07

CurlyMo

www.pilight.org

Topicstarter
Check

geen vragen via PM die ook op het forum gesteld kunnen worden.


  • R4gnax
  • Registratie: maart 2009
  • Laatst online: 17:43
En nu even voor de lol; een versie die point-free is gemaakt met de experimentele pipeline operator:

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const data = "id: 37\nevent: bar\ndata: a\n\nid: 38\nevent: foo\ndata: b\n\n";

const split = sep => str => str.trim().split( sep );
const map   = fnc => arr => arr.map( fnc );

const obj = data
  |> split( "\n\n" )
  |> map( item => item
    |> split( "\n" )
    |> map( prop => prop.split( /\s*:\s*(.+)/ ))
    |> Object.fromEntries
  );

console.log( JSON.stringify( obj, null, "  " ));

  • BarôZZa
  • Registratie: januari 2003
  • Laatst online: 02:07
R4gnax schreef op maandag 6 januari 2020 @ 23:09:
[...]


Ik denk dat @BarôZZa daar de plank een beetje misslaat.
Of tenminste; twee dingen door elkaar aan het halen is.

Dat je een recept in stap 1,2,3 volgt wil niet meteen zeggen dat het niet functional is. Puur functional gezien zou de notatie 3(2(1())) worden, of op een andere manier gecomposed, maar alsnog loop je door die stappen heen.
Niet echt toch?

Mijn kritiek was meer op de manier waarop alles gecomposed werd, dát voelde erg imperatief aan.

Het belangrijkste doel van functioneel programmeren is in mijn ogen tot logische, herbruikbare functies komen en zaken op die manier weten op te delen. Dus dan kom je bij twee zaken die je los kan trekken: een functie om een item samen te stellen(die het object met de attributes teruggeeft) en/of een functie die delen van de string parst. Geen van beide werd gedaan.

Bij de code in de startpost werd alles door elkaar gehosseld en vervolgens met wat creatieve reduce functies weer rechtgetrokken. De slice maakte bijvoorbeeld ook dat het specifiek alleen met een string werkt die eindigt met \n\n omdat je anders het laatste item mist. Dat zijn precies de zaken die je veel bij imperatief programmeren ziet (bouw tijdelijke arrays en op het laatst door alles loopen en het eindresultaat fixen).

Verder: als je met regex in een split gaat werken, dan kan je natuurlijk ook gewoon de hele string direct parsen met een regex :P
Pagina: 1


Apple iPhone 11 Microsoft Xbox Series X LG OLED C9 Google Pixel 4 CES 2020 Samsung Galaxy S20 4G Sony PlayStation 5 Nintendo Switch Lite

'14 '15 '16 '17 2018

Tweakers vormt samen met Hardware Info, AutoTrack, Gaspedaal.nl, Nationale Vacaturebank, Intermediair en Independer DPG Online Services B.V.
Alle rechten voorbehouden © 1998 - 2020 Hosting door True