부울 외부
31208 단어 rubyexternalitiesboolean
http://devblog.avdi.org/2014/09/17/boolean-externalities/
그의 게시물에서 그는 다음과 같은 질문을 합니다. 술어가 거짓을 반환하면 왜 그렇게 됩니까? 술어를 많이 연결하면 왜 답을 얻었는지 파악하기 어렵습니다.
이 예를 고려하십시오. 개체가 무서운지 확인하기 위해 간단한 연결 술어 논리를 구현합니다.
class SimpleBoo
def scary?
ghost? || zombie?
end
def ghost?
!alive? && regrets?
end
def zombie?
!alive? && hungry_for_brains?
end
def alive?
false
end
def regrets?
false
end
def hungry_for_brains?
false
end
end
일련의 논리를 따르면 유령이나 좀비라면 무서운 것입니다. 둘 다 살아 있지 않지만 유령은 후회하고 좀비는 두뇌에 굶주려 있습니다. 이것은 아마도 프로덕션 앱용으로 작성하는 코드입니다. 간단하고 읽기가 매우 쉽습니다.
단점은 무언가가 무서운 이유를 알고 싶다면 가서 코드를 읽어야 한다는 것입니다. 결론에 도달한 이유를 물체에게 물어볼 수 없습니다.
왜
다음은 코드 진화의 논리적 다음 단계입니다. 길이와 가독성에 엄청난 "비용"이 있지만 조건자가 true 또는 false를 반환하는 이유를 설명할 수 있도록 코드를 수정했습니다.
class WhyNotBoo
# The object is scary if there is a reason for it to be scary.
def scary?
why_scary.any?
end
# Why is this object scary?
def why_scary
reasons = []
# Early termination if this object is *not* scary.
return reasons unless ghost? || zombie?
# Recursively determine why this object is scary.
reasons.concat([:ghost => why_ghost]) if ghost?
reasons.concat([:zombie => why_zombie]) if zombie?
reasons
end
# For the "why not" question we re-implement the "why" logic in reverse.
def why_not_scary
reasons = []
return reasons if ghost? || zombie?
reasons.concat([:not_ghost => why_not_ghost]) unless ghost?
reasons.concat([:not_zombie => why_not_zombie]) unless zombie?
reasons
end
def ghost?
why_ghost.any?
end
def why_ghost
return [] unless !alive? && regrets?
[:not_alive, :regrets]
end
def why_not_ghost
reasons = []
return reasons if ghost?
reasons << :alive if alive?
reasons << :no_regrets unless regrets?
reasons
end
def zombie?
why_zombie.any?
end
def why_zombie
return [] unless !alive? && hungry_for_brains?
[:not_alive, :hungry_for_brains]
end
def why_not_zombie
reasons = []
return reasons if zombie?
reasons << :alive if alive?
reasons << :not_hungry_for_brains unless hungry_for_brains?
reasons
end
def alive?
true
end
def regrets?
false
end
def hungry_for_brains?
false
end
end
예, 훨씬 더 많은 코드입니다. 모든 복합 술어에는 "why_[술어]"및 "why_not_[술어]"버전이 있습니다. 이제 어떤 것이 무서운지, 왜 무서운지(또는 왜 안 좋은지) 물어볼 수 있습니다.
이 접근 방식에는 몇 가지 문제가 있습니다.
scary?
에 없습니다. why_scary
와 why_not_scary
사이에 복제됩니다. 반복하지 마십시오. 그렇지 않으면 논리 버그가 발생합니다. 클리너 코드
"why"와 "why not"의 기능을 유지하면서 코드를 다시 읽을 수 있게 만들 수 있는지 봅시다.
class ReasonBoo < EitherAll
def scary?
either :ghost, :zombie
end
def ghost?
all :not_alive, :regrets
end
def zombie?
all :not_alive, :hungry_for_brains
end
def alive?
false
end
def regrets?
false
end
def hungry_for_brains?
false
end
end
여태까지는 그런대로 잘됐다. 코드는 매우 읽기 쉽지만 미스터리한 수퍼클래스
EitherAnd
가 있습니다. 작동 방식을 살펴보기 전에 무엇을 할 수 있는지 살펴보겠습니다.boo = ReasonBoo.new
boo.scary? # => false
boo.why_scary # => []
boo.why_not_scary # => [{:not_ghost=>[:not_regrets]}, {:not_zombie=>[:not_hungry_for_brains]}]
boo.ghost? # => false
boo.why_ghost # => []
boo.why_not_ghost # => [:not_regrets]
boo.zombie? # => false
boo.why_zombie # => []
boo.why_not_zombie # => [:not_hungry_for_brains]
either
또는 all
를 사용하는 각 조건자에 대해 우리는 그것이 참인 이유 또는 이유를 물을 수 있으며 응답은 일련의 조건자 검사입니다.더 깨끗한 코드를 얻는 방법
코드를 읽을 수 있게 하려면 일반적으로 지저분한 배관 코드가 있어야 합니다. 이 예에서 우리는 이것을 슈퍼클래스에 숨겼지만 너무 많은 노력 없이도 모듈일 수 있었습니다.
코드를 더 쉽게 읽을 수 있도록 중복 논리를 도우미 메서드로 추출하지 않도록 선택했습니다.
이 클래스는
either
및 all
의 두 가지 메서드를 구현합니다.두 방법 모두 구조가 동일합니다.
class EitherAll
# This method mimics the behavior of "||". These two lines are functionally equivalent:
#
# ghost? || zombie? # => false
# either :ghost, :zombie # => false
#
# The bonus of `either` is that afterwards you can ask why or why not:
#
# why_not_scary # => [{:not_ghost=>[:not_regrets]}, {:not_zombie=>[:not_hungry_for_brains]}]
def either(*predicate_names)
#
# 1. Setup up the why_ and why_not_ methods
#
# Two arrays to track the why and why not reasons.
why_reasons = []
why_not_reasons = []
# This is a ruby 2.0 feature that replaces having to regexp parse the `caller` array.
# Our goal here is to determine the name of the method that called us.
# In this example it is likely to be the `scary?` method.
context_method_name = caller_locations(1, 1)[0].label
# Strip the trailing question mark
context = context_method_name.sub(/\?$/, '').to_sym
# Set instance variables for why and why not for the current context (calling method name).
# In our example, this is going to be @why_scary and @why_not_scary.
instance_variable_set("@why_#{context}", why_reasons)
instance_variable_set("@why_not_#{context}", why_not_reasons)
# Create reader methods for `why_scary` and `why_not_scary`.
self.class.class_eval do
attr_reader :"why_#{context}", :"why_not_#{context}"
end
#
# 2. Evaluate each predicate until one returns true
#
predicate_names.each do |predicate_name|
# Transform the given predicate name into a predicate method name.
# We check if the predicate needs to be negated, to support not_<predicate>.
predicate_name_string = predicate_name.to_s
if predicate_name_string.start_with?('not_')
negate = true
predicate_method_name = "#{predicate_name_string.sub(/^not_/, '')}?"
else
negate = false
predicate_method_name = "#{predicate_name_string}?"
end
# Evaluate the predicate
if negate
# Negate the return value of a negated predicate.
# This simplifies the logic for our success case.
# `value` is always true if it is what we ask for.
value = !public_send(predicate_method_name)
else
value = public_send(predicate_method_name)
end
#
# 3. Track which predicates were true/false to explain *why* we got the answer we did.
#
if value
# We have a true value, so we found what we are looking for.
# If possible, follow the chain of reasoning by asking why the predicate is true.
if respond_to?("why_#{predicate_name}")
why_reasons << { predicate_name => public_send("why_#{predicate_name}") }
else
why_reasons << predicate_name
end
# Because value is true, clear the reasons why we would not be.
# They don't matter anymore.
why_not_reasons.clear
# To ensure lazy evaluation, we stop here.
return true
else
# We have a false value, so we continue looking for a true predicate
if negate
# Our predicate negated, so we want to use the non-negated version.
# In our example, if `alive?` were true, we are not a zombie because we are not "not alive".
# Our check is for :not_alive, so the "why not" reason is :alive.
negative_predicate_name = predicate_name_string.sub(/^not_/, '').to_sym
else
# Our predicate is not negated, so we need to use the negated predicate.
# In our example, we are not scary because we are not a ghost (or a zombie).
# Our check is for :scary, so the "why not" reason is :not_ghost.
negative_predicate_name = "not_#{predicate_name_string}".to_sym
end
# If possible, follow the chain of reasoning by asking why the predicate is false.
if respond_to?("why_#{negative_predicate_name}")
why_not_reasons << { negative_predicate_name => public_send("why_#{negative_predicate_name}") }
else
why_not_reasons << negative_predicate_name
end
end
end
# We failed because we did not get a true value at all (which would have caused early termination).
# Clear all positive reasons.
why_reasons.clear
# Explicitly return false to match style with the `return true` a few lines earlier.
return false
end
# This method works very similar to `either`, which is defined above.
# I'm only commenting on the differences here.
#
# This method mimics the behavior of "&&". These two lines are functionally equivalent:
#
# !alive? && hungry_for_brains?
# all :not_alive, :hungry_for_brains
def all(*predicate_names)
context_method_name = caller_locations(1, 1)[0].label
context = context_method_name.sub(/\?$/, '').to_sym
why_reasons = []
why_not_reasons = []
instance_variable_set("@why_#{context}", why_reasons)
instance_variable_set("@why_not_#{context}", why_not_reasons)
self.class.class_eval do
attr_reader :"why_#{context}", :"why_not_#{context}"
end
predicate_names.each do |predicate_name|
predicate_name_string = predicate_name.to_s
if predicate_name_string.start_with?('not_')
negate = true
predicate_method_name = "#{predicate_name_string.sub(/^not_/, '')}?"
else
negate = false
predicate_method_name = "#{predicate_name_string}?"
end
if negate
value = !public_send(predicate_method_name)
else
value = public_send(predicate_method_name)
end
# The logic is the same as `either` until here. The difference is:
#
# * Either looks for the first true to declare success
# * And looks for the first false to declare failure
#
# This means we have to reverse our logic.
if value
if respond_to?("why_#{predicate_name}")
why_reasons << { predicate_name => public_send("why_#{predicate_name}") }
else
why_reasons << predicate_name
end
else
if negate
negative_predicate_name = predicate_name_string.sub(/^not_/, '').to_sym
else
negative_predicate_name = "not_#{predicate_name_string}".to_sym
end
if respond_to?("why_#{negative_predicate_name}")
why_not_reasons << { negative_predicate_name => public_send("why_#{negative_predicate_name}") }
else
why_not_reasons << negative_predicate_name
end
why_reasons.clear
return false
end
end
why_not_reasons.clear
return true
end
end
결론
200줄 미만의 Ruby 코드와 자체 코드의 사소한 변경으로 부울이 값을 반환하는 이유에 대한 추적 가능성을 제공할 수 있습니다.
분명한 경우와 제한 사항에도 불구하고 메서드가
true
또는 false
를 반환하는 이유를 모르는 문제에 대한 잠재적인 해결책이 있다는 것을 아는 것이 좋습니다.
Reference
이 문제에 관하여(부울 외부), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/narnach/boolean-externalities-4n55텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)