Other Versicle and Response Elements

Lauds Responsory 

The other element which uses the double Versicle/Response pair directly is the Lauds Responsory (which is used only in the Sarum Use):

      <LaudsResponsory>

<versicle>I ascend to My Father, and your Father.</versicle>

<response>To my God, and your God, alleluia.</response>

      </LaudsResponsory>

There are no attributes, just the double-element structure.

A base class defines the parsing for this, minus the name:

class DoubleResponseElement: public MultiElementElement

{

public:

  explicit DoubleResponseElement(const std::string& inName, std::string_view inText);

  const std::string &

  getStartTagName() const override

  {

    return m_nameTag.getName();

  }

  std::span<std::string> getValues() const { return m_values.getValues(); }

private:

  NoAttributeTag m_nameTag;

  VersicleAndResponsePair m_values;

};

The constructor benefits from the refactoring we just did:

DoubleResponseElement::DoubleResponseElement(const std::string &inName,

                                             std::string_view inText):

    MultiElementElement(inText, inName),

    m_nameTag(inName)

{

  auto l = [&](const NoAttributeTag &inVal, const int inAttributes) {

    std::string_view rest(inText.substr(getLength()));

    adjustBetweenTags(rest);

    m_values.extract(rest, *this);

    adjustBetweenTags(rest);

    if (!rest.starts_with(getEndTag()))

      throw OfficeParseException(

          getStartTagName() + " with unexpected element before closing tag",

          rest);

    incrementLength(getEndTag().length());

  };

  TagSetter().set<NoAttributeTag>(m_nameTag, l, inText.substr(0, getLength()));

}

The Lauds Responsory just provides a defined name:

class LaudsResponsoryElement : public DoubleResponseElement

{

public:

  explicit LaudsResponsoryElement(std::string_view inText):

      DoubleResponseElement("LaudsResponsory", inText)

  {

  }

  ~LaudsResponsoryElement() override;

};

Chapter Responses

A Verse / Response model (a little generalized) is also present in the ChapterResponsesElement.  However, it's a little more complicated than in the other cases.

In some cases, there is a single pair of values:

      <ChapterResponses>

<versicle>In His days Judah shall be saved, and Israel shall dwell safely,</versicle>

<response>Behold, the days come, saith the Lord, that I will raise unto David a righteous branch, and a King shall reign and prosper, and shall execute judgment and justice in the earth * And this is his Name whereby He shall be called : The Lord our Righteousness</response>

      </ChapterResponses>

These actually encode a Responsory with repeated sub-parts, but for the XML representation it is entirely identical.  Because the Verse part of the responsory follows the initial response, this can sometimes look a little odd semantically, but in ensures that V. in the formatted output matches <versicle> in the input.

There are also perfectly ordinary Versicle/Response pairs:

      <ChapterResponses>

<versicle>A voice crying in the wilderness.</versicle>

<response>Prepare ye the way of the Lord : make straight a highway for our God.</response>

      </ChapterResponses>

In other cases, there are two pairs of values:

      <ChapterResponses type="extended">

<versicle>Shew the light of Thy countenance, and we shall be whole.</versicle>

<response>Come and save us, * O Lord God of Hosts.</response>

<versicle>The heathen shall fear thy Name, O Lord.</versicle>

<response>And all the kings of the earth thy majesty.</response>

      </ChapterResponses>

These encode both a responsory and a following versicle/response.

We also have seasonal references:

      <ChapterResponses ref="LENT" />

and references to a prior Lauds office:

      <ChapterResponses ref="Lauds"/>

In the implementations of the offices containing these elements, we hold a vector rather than an array of values and simply assign the values collected by the element.

All this means that both the tag class and the element class are a bit more elaborate.

ChapterResponsesTag

The tag itself defines a public enum for representing the seasonal references and returns an expected pair count based on whether the attribute type is set to "extended".  Lauds references are handled in a way similar to other classes with the same XML representation.

class ChapterResponsesTag : public BreviaryTag

{

public:

  enum class SeasonalReferenceType

  {

    None,

    Lent,

    MidLent,

    Passiontide,

    Easter

  };


  ChapterResponsesTag(): BreviaryTag(GetElementName()) {}

  ~ChapterResponsesTag() override;

  int getResponsePairCount() const { return m_extended ? 2 : 1; }

  bool isLaudsReference() const { return m_laudsReference; }

  SeasonalReferenceType getSeasonalType() const { return m_seasonalType; }

  static const std::string GetElementName() { return "ChapterResponses"; }

private:

  int allowedAttributeCount() const override { return 2; }

  std::span<std::string> getAllowedAttributes() const override;

  bool validate(std::string_view inAttribute,

                std::string_view inValue) const override;

  void setValue(std::string_view inAttribute,

                std::string_view inValue) override;

  bool checkMandatoryAttributes() const override

  {

    if (hasAttribute("ref"))

      return isClosed();

    else

      return !isClosed();

  }

  bool m_extended = false;

  bool m_laudsReference = false;

  SeasonalReferenceType m_seasonalType = SeasonalReferenceType::None;

};


std::span<std::string>

ChapterResponsesTag::getAllowedAttributes() const

{

  static std::array<std::string, 2> rval{ "type", "ref" };

  return rval;

}

We don't expect to get "type" set except when extended, so we restrict the acceptable values accordingly.  We could have allowed other values just not to do anything, but that would mean that typos are interpreted incorrectly.

bool ChapterResponsesTag::validate(std::string_view inAttribute,

                              std::string_view inValue) const

{

  if (inAttribute == "type")

    return (inValue == "extended");

  else

    return ((inValue == "Lauds") || (inValue == "EASTER")

            || (inValue == "LENT") || (inValue == "MID_LENT")

            || (inValue == "PASSIONTIDE"));

}

void ChapterResponsesTag::setValue(std::string_view inAttribute,

                              std::string_view inValue)

{

  if (inAttribute == "type")

    m_extended = (inValue == "extended");

  else

    {

      if (inValue == "Lauds")

        m_laudsReference = (inValue == "Lauds");

      else if (inValue == "EASTER")

        {

          m_seasonalType = SeasonalReferenceType::Easter;

        }

      else if (inValue == "LENT")

        {

          m_seasonalType = SeasonalReferenceType::Lent;

        }

      else if (inValue == "MID_LENT")

        {

          m_seasonalType = SeasonalReferenceType::MidLent;

        }

      else if (inValue == "PASSIONTIDE")

        {

          m_seasonalType = SeasonalReferenceType::Passiontide;

        }

    }

}

ChapterResponsesElement

The basic interface to the ChapterResponsesElement is fairly straightforward.  Note that getValues() returns a copy of the stored vector: it will normally be assigned and the compile should be able to use the move version of the assignment operator.

class ChapterResponsesElement : public MultiElementElement

{

public:

  ChapterResponsesElement(std::string_view inText, const OfficeNames inOffice);


  // Second vespers

  ChapterResponsesElement(std::string_view inText,

                          const ILaudsChapter &inChapter,

                          const OfficeNames inOffice);


  ~ChapterResponsesElement() override;


  const std::vector<std::string> getValues() const { return m_values; }

  void extractResponses(std::string_view inText);

  const std::string &getStartTagName() const override

  {

    return m_tag.getName();

  }

private:

  ChapterResponsesTag m_tag;

  mutable std::vector<std::string> m_values;

};

Much of the functionality of the element's constructor is delegated to a separate local class:

  class ContentProcessor

  {

  public:

    ContentProcessor(std::string_view inText,

                     ChapterResponsesElement &inParent,

                     std::vector<std::string> &inValues,

                     const OfficeNames inOffice):

        m_parent(inParent),m_values(inValues),

        m_text(inText), m_office(inOffice)

    { }

    virtual ~ContentProcessor();

    void operator()(const ChapterResponsesTag &inVal, const int inAttributes);

  protected:

    virtual bool handleLaudsReference(const ChapterResponsesTag &inVal) = 0;

    static void ProcessSeasonalReference(

        std::vector<std::string> &outValues,

        const ChapterResponsesTag::SeasonalReferenceType inType,

        const OfficeNames inOffice);

  protected:

    ChapterResponsesElement &m_parent;

    std::vector<std::string> &m_values;

  private:

    std::string_view m_text;

    const OfficeNames m_office;

  };

In this case, with two constructors, it seemed better to capture commonalities of logic in a single class rather than use two different lambdas to pass to the tag setter. The variances are represented by the single template method virtual function handleLaudsReference().

void ChapterResponsesElement::ContentProcessor::operator()(

    const ChapterResponsesTag &inVal, const int inAttributes)

{

  if (inVal.isClosed())

    {

      if (!handleLaudsReference(inVal))

        ProcessSeasonalReference(m_values, inVal.getSeasonalType(), m_office);

      return;

    }

  else

    m_parent.extractResponses(m_text);

}

This involves rather more purely syntactic overhead to capture the references explicitly which would normally happen silently.  On the other hand, it also allows the seasonal reference handling to be entirely in the helper class:

I won't show the entire function, as the number of literal values makes it lengthy, but you can get an excellent idea from the beginning:

void ChapterResponsesElement::ContentProcessor::ProcessSeasonalReference(

    std::vector<std::string> &outValues,

    const ChapterResponsesTag::SeasonalReferenceType inType,

    const OfficeNames inOffice)

{

  switch (inType)

    {

      using enum ChapterResponsesTag::SeasonalReferenceType;

    case Lent:

      switch (inOffice)

        {

          using enum OfficeNames;

        case TERCE:

          {

            const static std::array<std::string, 4> vals{

              "O look Thou upon me, and be merciful unto me, as Thou usest to do unto those that love Thy Name."s,

              "Make me a companion of all them that fear Thee * and keep Thy commandments."s,

              "I will say unto the Lord, Thou art my hope, and my strong hold."s,

              "My God, in Him will I trust."s

            };

            std::ranges::copy(vals, std::back_inserter(outValues));

          }

          break;

...

All the other cases look the same except for the text values in the arrays.

The main class method extractResponses() uses the VersicleAndResponsePair class, but here not as storage; the values it captures are moved into the vector:

void ChapterResponsesElement::extractResponses(std::string_view inText)

{

  std::string_view rest(inText.substr(getLength()));

  std::ranges::for_each(

      std::ranges::iota_view{ 0, m_tag.getResponsePairCount() },

      [&](const auto ref) {

        adjustBetweenTags(rest);

        VersicleAndResponsePair vr;

        vr.extract(rest, *this);

        std::ranges::move(vr.getValues(), std::back_inserter(m_values));

      });

  if (m_values.empty())

    {

      throw OfficeParseException(

          "Must have at least one versicle/response pair for "

          + getStartTagName());

    }

  adjustBetweenTags(rest);

  if (!rest.starts_with(getEndTag()))

    throw OfficeParseException(

        getStartTagName() + " with unexpected element before closing tag: "

        + std::string(rest.substr(0, 20)));

  incrementLength(getEndTag().length());

}

Because the two constructors need to specialize the helper class, they are a little wordier than if they had simply used lambdas:

ChapterResponsesElement::ChapterResponsesElement(std::string_view inText,

                                                 const OfficeNames inOffice):

    MultiElementElement(inText, ChapterResponsesTag::GetElementName())

{

  class StandardProcessor : public ContentProcessor

  {

  public:

    StandardProcessor(std::string_view inText,

                      ChapterResponsesElement &inParent,

                      std::vector<std::string> &inValues,

                      const OfficeNames inOffice):

        ContentProcessor(inText, inParent, inValues, inOffice)

    { }

    bool handleLaudsReference(const ChapterResponsesTag &inVal) override

    {

      if (inVal.isLaudsReference())

        throw OfficeParseException(

            "Cannot have a reference " + m_parent.getStartTagName()

            + " element for antiphons other than second vespers");

      return false;

    }

  };

  StandardProcessor sp(inText, *this, m_values, inOffice);

  TagSetter().set<ChapterResponsesTag>(m_tag, sp,

                                       inText.substr(0, getLength()));

}


ChapterResponsesElement::ChapterResponsesElement(

    std::string_view inText, const ILaudsChapter &inChapter,

    const OfficeNames inOffice):

    MultiElementElement(inText, ChapterResponsesTag::GetElementName())

{

  class LaudsProcessor : public ContentProcessor

  {

  public:

    LaudsProcessor(const ILaudsChapter &inChapter, std::string_view inText,

                   ChapterResponsesElement &inParent,

                   std::vector<std::string> &inValues,

                   const OfficeNames inOffice):

        ContentProcessor(inText, inParent, inValues, inOffice),

        m_chapter(inChapter)

    { }

    bool handleLaudsReference(const ChapterResponsesTag &inVal) override

    {

      if (inVal.isLaudsReference())

        {

          std::ranges::copy(m_chapter.getResponses(),

                            std::back_inserter(m_values));

          return true;

        }

      else

        return false;

    }

  private:

    const ILaudsChapter &m_chapter;

  };

  LaudsProcessor lp(inChapter, inText, *this, m_values, inOffice);

  TagSetter().set<ChapterResponsesTag>(m_tag, lp,

                                       inText.substr(0, getLength()));

}

This raises an interesting point.

The version of the class which does the same things but uses two lambdas in the constructors and uses no helper class is lexically shorter even with the repetition of logic -- a mark in favour of the utility of lambdas -- but it does repeat the same logic with the only variance being how Lauds references are handled.  Can we make a clear decision on which approach is better?

Certainly if there were more variations -- other constructors which used the same logic, or (especially) other classes which could use a (suitably tweaked, probably template) version of the helper class -- then the balance would tip decisively in favour of the approach taken.  There's a general rule of three: one can live with two repetitions of something, but when you're going to introduce a third it's time to refactor the commonalities out.

If there were fewer references to capture, reducing the textual overhead of the classes, that would also tip the balance, because the extra syntactic requirements would be reduced. (If C++ were like Java in giving local classes access to the parent members, the classes would be almost as simple as the lambdas.  You could even do the two specialized versions as anonymous extensions of the base class.  But this isn't Java.)

I tend to come down on the side of reducing logic repetition, because in real life situations it's a good habit to get into.  In my experience a single repetition usually means that at some point in the future you will need to create another reuse variant. (If I wanted to handle the responsories in the Night Office/Mattins this would happen here.)  It's cheaper to future-proof your code upfront.  Similarly, even with two instances, if you find a bug, or need to do a logic extension touching both of them, it's extra work and a potential cause of bugs.  So I'll live with the extra syntactic overhead (all of which the compiler will ensure is correct, as it's essentially all capturing references) and get the logic in one place.  Simpler does not always mean shorter.  (And even with the additional overhead, the extra lines in the constructors don't make them longer than part of a single screen.)

Comments

Popular posts from this blog

Boundaries

State Machines

Considerations on an Optimization