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.