diff --git a/config.json b/config.json index b8928a2ca5..e2a25ba98a 100644 --- a/config.json +++ b/config.json @@ -2621,6 +2621,22 @@ "errors" ], "difficulty": 5 + }, + { + "slug": "lens-person", + "name": "Lens Person", + "uuid": "a1e71425-0e7e-442a-9c8e-cc252f440760", + "practices": [], + "prerequisites": [ + "prototypes-and-classes", + "callbacks" + ], + "difficulty": 7, + "topics": [ + "lens", + "classes", + "callbacks" + ] } ] }, diff --git a/exercises/practice/lens-person/.docs/instructions.md b/exercises/practice/lens-person/.docs/instructions.md new file mode 100644 index 0000000000..c7c707e249 --- /dev/null +++ b/exercises/practice/lens-person/.docs/instructions.md @@ -0,0 +1,15 @@ +# Instructions + +Use lenses to update nested records (specific to languages with immutable data). + +Updating fields of nested, immutable records is kind of annoying. +The code for such cases is as cumbersome as the structure is deep. +If you have, say, a Person, that contains an Address, which has a Street, that has a Number, updating the Number requires creating a new Street with the new Number, then a new Address with the new Street and, finally, a new Person with the new Address. +Confused already? + +One solution to this problem is to use [lenses][lenses]. + +Implement several record accessing functions using lenses. +The test suite also allows you to avoid lenses altogether so you can experiment with different approaches. + +[lenses]: https://en.wikibooks.org/wiki/Haskell/Lenses_and_functional_references diff --git a/exercises/practice/lens-person/.docs/introduction.md b/exercises/practice/lens-person/.docs/introduction.md new file mode 100644 index 0000000000..2cc3a206d7 --- /dev/null +++ b/exercises/practice/lens-person/.docs/introduction.md @@ -0,0 +1,7 @@ +In JavaScript, lenses are a functional programming concept that allows you to access and modify data in a modular and immutable way. They are essentially composable pairs of pure getter and setter functions that focus on a particular field inside an object. + +Lenses can be used to simplify code, make it more reusable, and avoid common programming errors. For example, lenses can be used to: + +- Access and modify nested data structures without having to worry about the specific structure of the data. +- Update data in a pure way, without mutating the original object. +- Compose multiple lenses together to create more complex lenses that can access and modify data in a variety of ways. \ No newline at end of file diff --git a/exercises/practice/lens-person/.eslintrc b/exercises/practice/lens-person/.eslintrc new file mode 100644 index 0000000000..1d4446029c --- /dev/null +++ b/exercises/practice/lens-person/.eslintrc @@ -0,0 +1,14 @@ +{ + "root": true, + "extends": "@exercism/eslint-config-javascript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [".meta/proof.ci.js", ".meta/exemplar.js", "*.spec.js"], + "excludedFiles": ["custom.spec.js"], + "extends": "@exercism/eslint-config-javascript/maintainers" + } + ] +} diff --git a/exercises/practice/lens-person/.gitignore b/exercises/practice/lens-person/.gitignore new file mode 100644 index 0000000000..31c57dd53a --- /dev/null +++ b/exercises/practice/lens-person/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/pnpm-lock.yaml +/yarn.lock diff --git a/exercises/practice/lens-person/.meta/config.json b/exercises/practice/lens-person/.meta/config.json new file mode 100644 index 0000000000..a9e8809f4e --- /dev/null +++ b/exercises/practice/lens-person/.meta/config.json @@ -0,0 +1,25 @@ +{ + "authors": [ + "sarava338", + "Cool-Katt" + ], + "files": { + "solution": [ + "lens-person.js" + ], + "test": [ + "lens-person.spec.js" + ], + "example": [ + ".meta/proof.ci.js" + ], + "editor": [ + "address.js", + "born.js", + "lens.js", + "name.js", + "person.js" + ] + }, + "blurb": "Use lenses to update nested records (specific to languages with immutable data)." +} diff --git a/exercises/practice/lens-person/.meta/proof.ci.js b/exercises/practice/lens-person/.meta/proof.ci.js new file mode 100644 index 0000000000..7adcc61aac --- /dev/null +++ b/exercises/practice/lens-person/.meta/proof.ci.js @@ -0,0 +1,39 @@ +/* eslint-disable no-unused-vars */ +import { Person } from '../person'; +import { Name } from '../name'; +import { Born } from '../born'; +import { Address } from '../address'; +import { Lens } from '../lens'; + +// Implement the nameLens with the getter and setter +export const nameLens = new Lens( + (person) => person.name, + (person, name) => new Person(name, person.born, person.address), +); + +// Implement the bornAtLens with the getter and setter +export const bornAtLens = new Lens( + (person) => person.born.bornAt, + (person, bornAt) => + new Person( + person.name, + new Born(bornAt, person.born.bornOn), + person.address, + ), +); + +// Implement the streetLens with the getter and setter +export const streetLens = new Lens( + (person) => person.address.street, + (person, street) => + new Person( + person.name, + person.born, + new Address( + person.address.houseNumber, + street, + person.address.place, + person.address.country, + ), + ), +); diff --git a/exercises/practice/lens-person/.npmrc b/exercises/practice/lens-person/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/practice/lens-person/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/lens-person/LICENSE b/exercises/practice/lens-person/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/practice/lens-person/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/lens-person/address.js b/exercises/practice/lens-person/address.js new file mode 100644 index 0000000000..8920561a85 --- /dev/null +++ b/exercises/practice/lens-person/address.js @@ -0,0 +1,15 @@ +export class Address { + /** + * + * @param {number} houseNumber + * @param {string} street + * @param {string} place + * @param {string} country + */ + constructor(houseNumber, street, place, country) { + this.houseNumber = houseNumber; + this.street = street; + this.place = place; + this.country = country; + } +} diff --git a/exercises/practice/lens-person/babel.config.js b/exercises/practice/lens-person/babel.config.js new file mode 100644 index 0000000000..b781d5a667 --- /dev/null +++ b/exercises/practice/lens-person/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@exercism/babel-preset-javascript'], + plugins: [], +}; diff --git a/exercises/practice/lens-person/born.js b/exercises/practice/lens-person/born.js new file mode 100644 index 0000000000..a949d89daa --- /dev/null +++ b/exercises/practice/lens-person/born.js @@ -0,0 +1,11 @@ +export class Born { + /** + * + * @param {Address} bornAt + * @param {Date} bornOn + */ + constructor(bornAt, bornOn) { + this.bornAt = bornAt; + this.bornOn = bornOn; + } +} diff --git a/exercises/practice/lens-person/lens-person.js b/exercises/practice/lens-person/lens-person.js new file mode 100644 index 0000000000..2a07feb382 --- /dev/null +++ b/exercises/practice/lens-person/lens-person.js @@ -0,0 +1,41 @@ +// +// This is only a SKELETON file for the 'Lense Person' exercise. It's been provided as a +// convenience to get you started writing code faster. +// + +/* eslint-disable no-unused-vars */ +import { Person } from './person'; +import { Name } from './name'; +import { Born } from './born'; +import { Address } from './address'; +import { Lens } from './lens'; + +// Implement the nameLens with the getter and setter +export const nameLens = new Lens( + () => { + throw new Error('Remove this statement and implement this function'); + }, + () => { + throw new Error('Remove this statement and implement this function'); + }, +); + +// Implement the bornAtLens with the getter and setter +export const bornAtLens = new Lens( + () => { + throw new Error('Remove this statement and implement this function'); + }, + () => { + throw new Error('Remove this statement and implement this function'); + }, +); + +// Implement the streetLens with the getter and setter +export const streetLens = new Lens( + () => { + throw new Error('Remove this statement and implement this function'); + }, + () => { + throw new Error('Remove this statement and implement this function'); + }, +); diff --git a/exercises/practice/lens-person/lens-person.spec.js b/exercises/practice/lens-person/lens-person.spec.js new file mode 100644 index 0000000000..b92a0de2ad --- /dev/null +++ b/exercises/practice/lens-person/lens-person.spec.js @@ -0,0 +1,95 @@ +import { Person } from './person'; +import { Name } from './name'; +import { Address } from './address'; +import { Born } from './born'; + +// import { nameLens, bornAtLens, streetLens } from './.meta/proof.ci'; +import { nameLens, bornAtLens, streetLens } from './lens-person'; + +// test data +const person = new Person( + new Name('Saravanan', 'Lakshamanan'), + new Born( + new Address(100, 'Hospital street', 'Tamil Nadu', 'India'), + new Date(), + ), + new Address(1, 'Coder street', 'Tamil Nadu', 'India'), +); + +// test suite for nameLens +describe('nameLens', () => { + test('should get the name of the person', () => { + expect(nameLens.get(person)).toEqual(person.name); + }); + + xtest('should set a new forename for the person', () => { + const updatedPerson = nameLens.set(person, new Name('Sara', 'Lakshmanan')); + expect(nameLens.get(updatedPerson)).toEqual(updatedPerson.name); + }); + + xtest('should set a new surname for the person', () => { + const updatedPerson = nameLens.set(person, new Name('Saravanan', 'Laksh')); + expect(nameLens.get(updatedPerson)).toEqual(updatedPerson.name); + }); + + xtest('should ensure immutability by checking the original person object', () => { + expect(person).not.toStrictEqual( + new Person(new Name('Sara', 'Lakshamanan'), person.born, person.address), + ); + }); +}); + +// Test suite for bornAtLens +describe('bornAtLens', () => { + xtest('should get the address where the person was born', () => { + expect(bornAtLens.get(person)).toEqual(person.born.bornAt); + }); + + xtest('should set a new street for the place where the person was born', () => { + const updatedPerson = bornAtLens.set( + person, + new Address(2, 'Exercism street', 'Tamil Nadu', 'India'), + ); + expect(bornAtLens.get(updatedPerson)).toEqual(updatedPerson.born.bornAt); + }); + + xtest('should ensure immutability by checking the original person object', () => { + expect(person).not.toEqual( + new Person( + person.name, + new Born( + new Address(2, 'Exercism street', 'Tamil Nadu', 'India'), + person.born.bornOn, + ), + person.address, + ), + ); + }); +}); + +// Test suite for streetLens +describe('streetLens', () => { + xtest('should get the current street of the person', () => { + expect(streetLens.get(person)).toEqual(person.address.street); + }); + + xtest('should set a new street for the current address of the person', () => { + const updatedPerson = streetLens.set(person, 'Exercism street'); + expect(streetLens.get(updatedPerson)).toEqual(updatedPerson.address.street); + }); + + xtest('should ensure immutability by checking the original person object', () => { + expect(person).not.toEqual( + new Person( + person.name, + person.born, + new Address( + person.address.houseNumber, + 'Exercism Street', + person.address.place, + person.address.country, + ), + ), + ); + }); +}); diff --git a/exercises/practice/lens-person/lens.js b/exercises/practice/lens-person/lens.js new file mode 100644 index 0000000000..4215d500ab --- /dev/null +++ b/exercises/practice/lens-person/lens.js @@ -0,0 +1,30 @@ +export class Lens { + /** + * + * @param {Function} getter + * @param {Function} setter + */ + constructor(getter, setter) { + this.get = getter; + this.set = setter; + } + + /** + * Function to get the value from a lens + * @param {Person} person + * @returns {Person} + */ + get(person) { + return this.get(person); + } + + /** + * Function to set the value using a lens + * @param {Person} person + * @param {any} value + * @returns {Person} + */ + set(person, value) { + return this.set(value, person); + } +} diff --git a/exercises/practice/lens-person/name.js b/exercises/practice/lens-person/name.js new file mode 100644 index 0000000000..d254e2552a --- /dev/null +++ b/exercises/practice/lens-person/name.js @@ -0,0 +1,11 @@ +export class Name { + /** + * + * @param {string} forename + * @param {string} surname + */ + constructor(forename, surname) { + this.forename = forename; + this.surname = surname; + } +} diff --git a/exercises/practice/lens-person/package.json b/exercises/practice/lens-person/package.json new file mode 100644 index 0000000000..eb78779878 --- /dev/null +++ b/exercises/practice/lens-person/package.json @@ -0,0 +1,35 @@ +{ + "name": "@exercism/javascript-lens-person", + "description": "Exercism practice exercise on lens-person", + "author": "Katrina Owen", + "contributors": [ + "Derk-Jan Karrenbeld (https://derk-jan.com)", + "Tejas Bubane (https://tejasbubane.github.io/)", + "Cool-Katt (https://github.com/Cool-Katt)", + "sarava338 (https://github.com/sarava338)" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/practice/lens-person" + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@exercism/babel-preset-javascript": "^0.2.1", + "@exercism/eslint-config-javascript": "^0.6.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.12.10", + "babel-jest": "^29.6.4", + "core-js": "~3.37.0", + "eslint": "^8.49.0", + "jest": "^29.7.0" + }, + "dependencies": {}, + "scripts": { + "test": "jest ./*", + "watch": "jest --watch ./*", + "lint": "eslint ." + } +} diff --git a/exercises/practice/lens-person/person.js b/exercises/practice/lens-person/person.js new file mode 100644 index 0000000000..0cce5f6b3b --- /dev/null +++ b/exercises/practice/lens-person/person.js @@ -0,0 +1,12 @@ +export class Person { + /** + * @param {Name} name + * @param {Born} born + * @param {Address} address + */ + constructor(name, born, address) { + this.name = name; + this.born = born; + this.address = address; + } +}