0

I have a struct and several boolean mappings that are used to keep track of struct's properties

struct Animal {
    string species;
    bytes32 ID;
}

mapping (bytes32 => Animal) public Animals;

mapping (bytes32 => bool) public Mammals;  
mapping (bytes32 => bool) public Birds;
mapping (bytes32 => bool) public Fish;

mapping (bytes32 => bool) public Carnivore;
mapping (bytes32 => bool) public Herbavore;
mapping (bytes32 => bool) public Omnivore;

After setting some of these booleans to "true" I want the user to call a functions to check on an animal's properties.

function CarnivoreFish(bytes32 _ID) public returns (bool){

    //used to check if animal is a carnivorous fish

    if( Fish[_ID] == true &&  Carnivore[_ID] ==true ){
       return true;
    }
    else{
        return false;
    }      
}

I want to be able to generalize the previous function to work for any combination of animal class and diet. I have tried defining new mappings and setting them equal to the mappings above based on keywords.

function filterAnimals(bytes32 _ID, string memory _animalClass, string memory _diet) public returns (bool){

    mapping (bytes32 => bool) memory AnimalClass;
    mapping (bytes32 => bool) memory Diet;

    //set first mapping "AnimalClass"
    if (_animalClass == "mammals") {
      AnimalClass = Mammals;  
    } 
    else if (_animalClass == "birds"){
       AnimalClass = Birds;
    }
    else{
        AnimalClass = Fish;
    }

    //set second mapping "Diet"
    if (_animalClass == "mammals") {
      Diet = Carnivore;  
    } 
    else if (_animalClass == "birds"){
       Diet = Herbavore;
    }
    else{
        Diet = Omnivore;
    }


    //check if animal has both propeties 

    if( AnimalClass[_ID] == true &&  Diet[_ID] ==true ){
       return true;
    }
    else{
        return false;
    }
}  

I receive errors based on the way I am trying to define my mappings. Is there a way to set one mapping equal to another like I am trying to do? Is there a more obvious way to generalize the CarnivoreFish function?

1

You cannot assign mappings inside a function even if the compiler seems to let you. That will lead to corrupted storage. The storage layout of persistent variables must be global. Temporary mappings functions inside functions are not allowed. Solc Compiler oversight? Innappropriate mapping declaration overwrites storage

You would want to reorganize the storage in any case. If I haven't miscounted, your way takes 10 words per animal versus 1 word (with 30 bytes to spare) as suggested below. I dropped the string because it takes two words by itself in the best-case scenario.

pragma solidity 0.5.1;

contract Animals {

    enum Branch {mammal, bird, fish, insect}
    enum Diet {carnivore, herbivore, onmivore}

    struct Animal {
        // string species; // use a bytes32 or drop entirely
        Branch branch;
        Diet diet;
    }

    mapping(bytes32 => Animal) public animals;

    function setAnimal(bytes32 id, Branch branch, Diet diet) public {
        Animal storage a = animals[id];
        a.branch = branch;
        a.diet = diet;
    }

    function animalIsBranch(bytes32 id, Branch branch) public view returns(bool isIndeed) {
        return animals[id].branch == branch;
    }
    function animalIsDiet(bytes32 id, Diet diet) public view returns(bool isIndeed) {
        return animals[id].diet == diet;
    }

    // now, the combinations
    function carnivoreFish(bytes32 id) public view returns(bool isIndeed) {
        return animalIsBranch(id, Branch.fish) && animalIsDiet(id, Diet.carnivore);
    }
    // keep going. 
}

The above approach is idiomatic and will work for a handful of combinations of traits. In case you want to further generalise, you might look at the handling of collections in Gnosis. It starts with tightly packed bools in a uint (bools pack into full bytes). This is a very simplified approach, inspired by their collection sets.

Something like this:

// 0b000.....00000000001 // mammal
// 00000.....00000000010 // bird
// 00000.....00000000100 // fish 
// 00000.....00000001000 // carnivore 
// 00000.....00000010000 // herbivore 
// 00000.....00000100000 // onmivore

.... = 256 bits to work with for all traits.

You can form queries by combining the bits into other numbers. For example, mammal and herbivore:

// 0b000.....00000100001 // mammal and herbivore

Your function would use bitwise operations to confirm all bits in your query are present in the data (or not).

  function isTrue(bytes32 id, uint profile) public view returns(bool isIndeed) {
    Animal storage a = animals[id];
    uint characteristics = a.characteristics;
    // A bitwise comparison determines what the answer means.
    // Is one of the profile bits present in characteristics bits? (OR)
    // Are all of the profile bits present in the characteristics bits? (AND)
  }

In practice, you would likely have two such functions, AND and OR. Let's go with this and say the example is tooled for AND.

You can combine such operations into more complex queries, e.g. all branches, AND herbivore.

  function or(bytes32 id, uint profile1, uint profile2) public view returns(bool isIndeed) {
    return isTrue(id, profile1) || isTrue(id, profile2);
  }

  function and(bytes32 id, uint profile1, uint profile2) public view returns(bool isIndeed) {
    return isTrue(id,profile1) && isTrue(id, profile2);
  }

Altogether:

contract AnimalsBitwise {

  struct Animal {
    uint characteristics;
  }

  mapping(bytes32 => Animal) public animals;

  function setAnimal(bytes32 id, uint characteristics) public { 
    // opporunity to validate.
    // disallow impossible creatures, e.g. impossible fish birds
  }

  function allTrue(bytes32 id, uint profile) public view returns(bool isIndeed) {
    Animal storage a = animals[id];
    uint characteristics = a.characteristics;
    // bitwise comparison.
    // Are ALL profile bits are present in characteristics bits?
  }
  function anyTrue(bytes32 id, uint profile) public view returns(bool isIndeed) {
    Animal storage a = animals[id];
    uint characteristics = a.characteristics;
    // bitwise comparison.
    // Is ANY profile bit present in characteristics bits?
  }
  function or(bytes32 id, uint profile1, uint profile2) public view returns(bool isIndeed) {
    return allTrue(id, profile1) || allTrue(id, profile2);
  }

  function and(bytes32 id, uint profile1, uint profile2) public view returns(bool isIndeed) {
    return allTrue(id,profile1) && allTrue(id, profile2);
  }
}

If you build out for completeness, you might be able to say that boolean queries of arbitrary complexity are possible over a set of 256 possible traits at a reasonable cost. You might even be able to compose a simple grammar, passing in arrays of operations to perform.

There would be iteration in that case (we don't like iteration), so complexity would ultimately be bound by the gasLimit. One would think desireable queries would not need more than a handful of steps provided the primitive are carefully worked out to cover all cases.

I hope it gives you some ideas.

  • 2
    Thanks for all the great info! Will take a while to consider my options! – Bryan Campbell Jul 19 at 0:53
  • How do you access the nth bit in uint charecteristics to check for a specific trait? – Bryan Campbell Jul 19 at 12:54
  • Ah I get it. I can use AND operator with charecteristics and 0000010000 with 1 as the nth bit. Thanks again – Bryan Campbell Jul 19 at 20:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.