Arjen Markus (13 march 2020) The goal of the program below is to see how you could use simple rules as a basis for reasoning about some phenomenon. The very simple example here concerns questions about the suitability of some habitat for two species, rabbits and foxes.
Note: The code is meant to be useable for other types of questions, but I have not tried that. But it is a the reason you can define questions yourself and that the command names are neutral.
The idea here is: an environment should facilitate the living conditions for the species in question and for these species there should be food. In the example: both rabbits and foxes dig holes, so the soil should be loose enough to allow them to dig holes. They both live predominantly in temperate climates, hence the condition that the summer temperature should belong to such climates (to keep the example small, I do not bother about the winter temperature).
Both species need to eat: rabbits are assumed to be content when there is enough green stuff around, but foxes eat rabbits. So, for foxes to live somewhere, there must be rabbits.
The questions and conditions are set up via the following code:
# habitat_rules.tcl -- # Sample collection of rules: # - The requirements for rabbits to live in a particular environment # - Similarly for foxes # source habitat.tcl # # Describe items # question summer-temperature "What is the typical summer temperature? (degrees C)" # # Define soil types: both species want to dig holes # # And herbs - it may be a logical parameter, but something needs to be done ... # choices soil "Soil types" {rock clay sand} choices herbs "Are there many herbs?" {yes no} ;# Should be automatic required soil rabbit sand required soil fox sand required food fox rabbit required food rabbit herbs required summer-temperature rabbit between 15 28 required summer-temperature fox between 15 28
With this base of rules set up, you can ask:
suitable rabbit
or:
# This one should work, but we need to answer questions about "rabbit" along the way suitable fox
Hidden in the code that helps to define and store the rules and to "reason" about the species, are a few details that I would like to explain now.
First of all, there are two types of conditions (even though the optional conditions are not implemented within the reasoning part yet):
The reasoning is purely logical: a condition is met or not, no nuances. That is a severe restriction. A kind of fuzzy reasoning might be more realistic, but I have not considered the consequences.
A condition, whether required or optional, may relate to a continuous variable, like the summer temperature, or to some choice. A particularly subtle one during the implementation was a logical condition - are there enough herbs? I wanted to formulate the condition as:
required food rabbit herbs
instead of something like:
required food rabbit herbs yes
The latler feels awkward. So that is handled via a "hack": if the choices are "yes" and "no", then the condition is considered to be logical - the choice "yes" is implicit.
And then, of course, a condition may be specific for a species (food) or it may be general (soil). This is handled by the slightly complicated in the procedures suitable and Ask.
Oh, and because a species may eat other species, the procedure suitable is used recursively.
The code for the actual reasoning is this:
# habitat.tcl -- # Attempt to use straightforward knowledge rules to reason about the suitability # of environmental and other conditions for organisms # # Note: # While I use the terminology of ecology, the methods are actually much more # general. # namespace eval ::habitat { variable choices [dict create] variable requirements [dict create] variable options [dict create] } # choices -- # Record the possible choices for a feature # # Arguments: # feature Name of the feature # question String to display when asking for its value # choices List of choices # # Result: # Choices as given are stored # proc choices {feature question choices} { dict set ::habitat::choices $feature question $question dict set ::habitat::choices $feature choices $choices } # question -- # Record the question to ask for a feature # # Arguments: # feature Name of the feature # question String to display when asking for its value # # Result: # The question as given is stored # proc question {feature question} { dict set ::habitat::choices $feature question $question } # required -- # Record a requirement regarding some feature for a species # # Arguments: # feature Name of the feature in question # species Name of the species # args Details of the requirement # # Result: # The requirement and its parameters are stored # proc required {feature species args} { if { [llength $args] == 1 } { dict set ::habitat::requirements $species $feature value [lindex $args 0] } elseif { [lindex $args 0] eq "between" } { dict set ::habitat::requirements $species $feature range [lrange $args 1 2] } elseif { [lindex $args 0] eq "lower" } { dict set ::habitat::requirements $species $feature max [lrange $args 1] } elseif { [lindex $args 0] eq "greater" } { dict set ::habitat::requirements $species $feature min [lrange $args 1] } } # optional -- # Record an optional condition regarding some feature for a species # # Arguments: # feature Name of the feature in question # species Name of the species # args Details of the condition # # Result: # The condition and its parameters are stored # proc optional {feature species args} { if { [llength $args] == 1 } { dict set ::habitat::options $species $feature value [lindex $args 0] } elseif { [lindex $args 0] eq "between" } { dict set ::habitat::options $species $feature range [lrange $args 1 2] } elseif { [lindex $args 0] eq "lower" } { dict set ::habitat::options $species $feature max [lrange $args 1] } elseif { [lindex $args 0] eq "greater" } { dict set ::habitat::options $species $feature min [lrange $args 1] } } # Ask -- # Ask for the value of a feature # # Arguments: # feature Name of the feature # value Registered value, may be necessary to get the right question # # Result: # Either a straightforward question or a list of choices is presented, # the value is recorded # proc ::habitat::Ask {feature value} { if { [dict exists $::habitat::values $feature] || [dict exists $::habitat::values $value] } { return } #puts $::habitat::choices #puts "$feature -- $value" # #puts "[expr { [dict exists $::habitat::choices $feature choices]}] -- [expr { [dict exists $::habitat::choices $value choices] }]" if { [dict exists $::habitat::choices $feature choices] || [dict exists $::habitat::choices $value choices] } { puts "Get question" if { [dict exists $::habitat::choices $feature choices] } { set question [dict get $::habitat::choices $feature question] set choices [dict get $::habitat::choices $feature choices] } else { set question [dict get $::habitat::choices $value question] set choices [dict get $::habitat::choices $value choices] } while {1} { puts "$question:" set n 0 foreach c $choices { incr n puts "$n. $c" } puts -nonewline "Enter the number: " flush stdout gets stdin in if { $in <= 0 || $in > $n } { puts "Value out of range - please try again ..." } else { incr in -1 dict set ::habitat::values $feature [lindex $choices $in] break } } } else { set question [dict get $::habitat::choices $value question] puts -nonewline "$question " flush stdout gets stdin value dict set ::habitat::values $feature $value } } # IsSpecies -- # Is the name a "species"? # # Arguments: # species Name of the species # # Result: # 1 if the name appears to be a species and 0 if not # proc ::habitat::IsSpecies {species} { expr { [dict exists $::habitat::requirements $species] || [dict exists $::habitat::options $species] } } # GetRequiredFeatures -- # Returned a list of required features # # Arguments: # species Name of the species # # Result: # List of registered features # proc ::habitat::GetRequiredFeatures {species} { set features {} foreach {feature storedValues} [dict get $::habitat::requirements $species] { lappend features $feature } return $features } # suitable -- # Determine if the habitat is suitable for a species # # Arguments: # species Name of the species # recurse (Optional) On the first call in the chain, clear the feature values # # Result: # Whether the habitat is suitable (1) or not (0) # proc suitable {species {recurse 0} } { if { ! $recurse } { set ::habitat::values [dict create] } puts "Suitability for \"$species\":" if { ! [::habitat::IsSpecies $species] } { puts "$species is not a species - at least: there are no requirements or optional conditions for it" return } # # First do the requirements: they must all be fulfilled # set suitable 1 foreach feature [::habitat::GetRequiredFeatures $species] { set property [dict get $::habitat::requirements $species $feature] #puts "$feature... $property" if { [lindex $property 0] eq "value" } { set value [lindex $property 1] if { [::habitat::IsSpecies $value] } { puts "Requirement $feature: $value ..." if { ! [suitable $value 1] } { puts " - requirement fails" set suitable 0 } } else { ::habitat::Ask $feature [dict get $::habitat::requirements $species $feature value] set value [dict get $::habitat::values $feature] # Hack: get logical values in if { $value != [lindex $property end] && $value != "yes" } { puts " - requirement fails" set suitable 0 } } } else { puts "Requirement $feature in range? ..." ::habitat::Ask $feature $feature set value [dict get $::habitat::values $feature] switch -- [lindex $property 0] { "range" { lassign [lindex $property end] min max #puts "min/max: $min -- $max" if { $value < $min || $value > $max } { set suitable 0 puts " - requirement fails" } } "min" { lassign $property dummy min if { $value < $min } { set suitable 0 puts " - requirement fails" } } "max" { lassign $property dummy max if { $value > $max } { set suitable 0 puts " - requirement fails" } default { puts " - keyword unknown: [lindex $property 0]" } } } } } if { $suitable && ! $recurse } { puts "- Obligatory requirements fulfilled!" } # # TODO: optimal conditions - at least one must be fulfilled # return $suitable }
This is a simple experiment with a very limited collection of examples. So a first possibility to improve this code is to demonstrate that it is too limited for more extensive or complicated sets of rules.
The optional conditions can be easily implemented without repeating the code of `suitable`.
Perhaps cosmetic, the dialogue may be improved. Some cluttering will occur and the messages are a bit too abstract.