Programming/JavaScript

[EloquentJS] Ch7. Project: A Robot

dododoo 2020. 4. 14. 20:05

Code

const roads = [
    "Alice's House-Bob's House",   "Alice's House-Cabin",
    "Alice's House-Post Office",   "Bob's House-Town Hall",
    "Daria's House-Ernie's House", "Daria's House-Town Hall",
    "Ernie's House-Grete's House", "Grete's House-Farm",
    "Grete's House-Shop",          "Marketplace-Farm",
    "Marketplace-Post Office",     "Marketplace-Shop",
    "Marketplace-Town Hall",       "Shop-Town Hall"
];

function buildGraph(edges) {
    // map으로 쓰기 위해
    let graph = Object.create(null);
    // 함수 내에서 반복되는 기능을 함수화
    function addEdge(from, to) {
        if (graph[from] == null) {
            graph[from] = [to];
        } else {
            graph[from].push(to);
        }
    }
    // forEach vs. map
    for (let [from, to] of edges.map(e => e.split("-"))) {
        addEdge(from, to);
        addEdge(to, from);
    }
    return graph;
}

const roadGraph = buildGraph(roads);

class VillageState {
    constructor(place, parcels) {
        this.place = place;
        this.parcels = parcels;
    }

    move(destination) {
        if (!roadGraph[this.place].includes(destination)) {
            return this;
        } else {
            // p를 수정하지 않도록 주의
            let parcels = this.parcels.map(p => {
                if (p.place != this.place) return p;
                return {place: destination, address: p.address};
            }).filter(p => p.place != p.address);
            return new VillageState(destination, parcels);
        }
    }
}

/*
let first = new VillageState(
    "Post Office",
    [{place: "Post Office", address: "Alice's House"}]
);
let next = first.next("Alice's House");

console.log(next.place);
// Alice's House
console.log(next.parcels);
// []
console.log(first.place);
// Post Office
*/

function runRobot(state, robot, memory)
{
    for (let turn = 0;; turn++) {
        if (state.parcels.length == 0) {
            console.log(`Done in ${turn} turns`);
            break;
        }
        let action = robot(state, memory);
        state = state.move(action.direction);
        memory = action.memory;
        console.log(`Moved to ${action.direction}`);
    }
}

function randomPick(array)
{
    let choice = Math.floor(Math.random() * array.length);
    return array[choice];
}

function randomRobot(state)
{
    return {direction: randomPick(roadGraph[state.place])};
}

// static method
VillageState.random = function(parcelCount = 5) {
    let parcels = [];
    for (let i = 0; i < parcelCount; i++) {
        let address = randomPick(Object.keys(roadGraph));
        let place;
        do {
            place = randomPick(Object.keys(roadGraph));
        } while (place == address);
        parcels.push({place, address});
    }
    return new VillageState("Post Office", parcels);
}

// run!
// runRobot(VillageState.random(), randomRobot);

const mailRoute = [
    "Alice's House", "Cabin", "Alice's House", "Bob's House",
    "Town Hall", "Daria's House", "Ernie's House",
    "Grete's House", "Shop", "Grete's House", "Farm",
    "Marketplace", "Post Office"
];

function routeRobot(state, memory)
{
    if (memory.length == 0) {
        memory = mailRoute;
    }
    return {direction: memory[0], memory: memory.slice(1)};
}

// run!
// runRobot(VillageState.random(), routeRobot, []);

function findRoute(graph, from, to)
{
    let work = [{at: from, route: []}];
    for (let i = 0; i < work.length; i++) {
        let {at, route} = work[i];
        for (let place of graph[at]) {
            if (place == to) return route.concat(place);
            if (!work.some(w => w.at == place)) {
                work.push({at: place, route: route.concat(place)});
            }
        }
    }
}

function goalOrientedRobot({place, parcels}, route)
{
    if (route.length == 0) {
        let parcel = parcels[0];
        if (parcel.place != place) {
            route = findRoute(roadGraph, place, parcel.place);
        } else {
            route = findRoute(roadGraph, place, parcel.address);
        }
    }
    return {direction: route[0], memory: route.slice(1)};
}

// run!
// runRobot(VillageState.random(), goalOrientedRobot, []);

// Ex. Measuring a robot
function runRobotSilently(state, robot, memory) {
    for (let turn = 0;; turn++) {
        if (state.parcels.length == 0) return turn;
        let action = robot(state, memory);
        // state, memory에 "대입"해도 함수 호출 밖의 state나 memory가 바뀌는 게 아님
        state = state.move(action.direction);
        memory = action.memory;       
    }
}

function compareRobots(robot1, memory1, robot2, memory2) {
    let tasks = 100;
    let steps1 = 0, steps2 = 0;
    for (let i = 0; i < tasks; ++i) {
        let state = VillageState.random();
        steps1 += runRobotSilently(state, robot1, memory1);
        steps2 += runRobotSilently(state, robot2, memory2);
    }
    console.log(`robot1: ${steps1/tasks}`);
    console.log(`robot2: ${steps2/tasks}`);
}

// compareRobots(routeRobot, [], goalOrientedRobot, []);

// Ex. Robot efficiency (**)
// 출제 의도에 맞지 않고 생각보다 빠르지 않음
function myRobot({place, parcels}, route) {
    if (route.length == 0) {
        let target = null;
        for (let p of parcels) {
            if (p.place != place) {
                target = p;
                break;
            }
        }
        if (target !== null) {
            route = findRoute(roadGraph, place, target.place);
        } else {
            route = findRoute(roadGraph, place, parcels[0].address);
        }
    }
    return {direction: route[0], memory: route.slice(1)};
}

// Solution
function yourRobot({place, parcels}, route) {
    if (route.length == 0) {
        let routes = parcels.map(p => {
            if (p.place != place) { 
                return {route: findRoute(roadGraph, place, p.place), pickUp: true};
            }
            else {
                return {route: findRoute(roadGraph, place, p.address), pickUp: false};
            }
        });

        function score({route, pickUp}) {
            return -route.length + (pickUp ? 0.5 : 0);  
        }

        route = routes.reduce((a, b) => {
            return score(a) > score(b) ? a : b;
        }).route;
    } 

    return {direction: route[0], memory: route.slice(1)};
}

// Ex. Persistent group (***)
class PGroup {
    constructor(elements) {
        this.elements = elements;
    }

    add(e) {
        //if (this.has(e)) return new PGroup(this.elements);
        if (this.has(e)) return this;
        else return new PGroup(this.elements.concat([e]));
    } 
    delete(e) {
        if (!this.has(e)) { 
            // return new PGroup(this.elements);
            return this;
        } else {
            return new PGroup(this.elements.filter(elem => elem !== e));
        }
    }   
    has(e) {
        return this.elements.includes(e);
    }
}

PGroup.empty = new PGroup([]);

The task

  • If you’re thinking in terms of object-oriented programming, your first impulse might be to start defining objects for the various elements in the world: a class for the robot, one for a parcel, maybe one for places. These could then hold properties that describe their current state, such as the pile of parcels at a location, which we could change when updating the world.
  • This is wrong.
  • At least, it usually is. The fact that something sounds like an object does not automatically mean that it should be an object in your program. Reflexively writing classes for every concept in your application tends to leave you with a collection of interconnected objects that each have their own internal, changing state. Such programs are often hard to understand and thus easy to break.
  • Instead, let’s condense the village’s state down to the minimal set of values that define it. There’s the robot’s current location and the collection of undelivered parcels, each of which has a current location and a destination address. That’s it.

Persistent data

  • Data structures that don’t change are called immutable or persistent. They behave a lot like strings and numbers in that they are who they are and stay that way, rather than containing different things at different times.
  • In JavaScript, just about everything can be changed, so working with values that are supposed to be persistent requires some restraint.
  • There is a function called Object.freeze that changes an object so that writing to its properties is ignored. You could use that to make sure your objects aren’t changed, if you want to be careful.
  • Freezing does require the computer to do some extra work, and having updates ignored is just about as likely to confuse someone as having them do the wrong thing. So I usually prefer to just tell people that a given object shouldn’t be messed with and hope they remember it.
  • let object = Object.freeze({value: 5});
    object.value = 10;
    console.log(object.value);
    // 5
  • Why am I going out of my way to not change objects when the language is obviously expecting me to?
  • Because it helps me understand my programs. This is about complexity management again. When the objects in my system are fixed, stable things, I can consider operations on them in isolation—moving to Alice’s house from a given start state always produces the same new state.
  • The most important limit on what kind of systems we can build is how much we can understand. Anything that makes your code easier to understand makes it possible to build a more ambitious system.
  • Unfortunately, although understanding a system built on persistent data structures is easier, designing one, especially when your programming language isn’t helping, can be a little harder. We’ll look for opportunities to use persistent data structures in this book, but we’ll also be using changeable ones.

Simulation

  • A delivery robot looks at the world and decides in which direction it wants to move. As such, we could say that a robot is a function that takes a VillageState object and returns the name of a nearby place.
  • Because we want robots to be able to remember things, so that they can make and execute plans, we also pass them their memory and allow them to return a new memory.
  • Thus, the thing a robot returns is an object containing both the direction it wants to move in and a memory value that will be given back to it the next time it is called.
  • What is the dumbest strategy that could possibly work? The robot could just walk in a random direction every turn.
  • We’ll first need a way to create a new state with some parcels. A static method (written here by directly adding a property to the constructor) is a good place to put that functionality.

Exercises

Persistent group

  • Most data structures provided in a standard JavaScript environment aren’t very well suited for persistent use. Arrays have slice and concat methods, which allow us to easily create new arrays without damaging the old one. But Set, for example, has no methods for creating a new set with an item added or removed.
  • The class’s constructor can take such an array as argument and store it as the instance’s (only) property. This array is never updated.
  • ...
  • You need only one empty instance because all empty groups are the same and instances of the class don’t change. You can create many different groups from that single empty group without affecting it.