C++ में ग्राफ़ बनाना

C++ ग्राफ़ बिल्डर इन कामों के लिए एक बेहतरीन टूल है:

  • जटिल ग्राफ़ बनाना
  • ग्राफ़ को पैरामेट्री करना (जैसे, InferenceCalculator पर डेलिगेट सेट करना, ग्राफ़ के हिस्सों को चालू/बंद करना)
  • डुप्लीकेट ग्राफ़ बनाना (उदाहरण के लिए, pbtxt में सीपीयू और जीपीयू के लिए बने ग्राफ़ के बजाय तो एक ही कोड बनाया जा सकता है जो ज़रूरी ग्राफ़ बनाता है. यह कोड जितना हो सके)
  • वैकल्पिक ग्राफ़ इनपुट/आउटपुट के साथ काम करना
  • प्रति प्लेटफ़ॉर्म ग्राफ़ कस्टमाइज़ करना

बुनियादी इस्तेमाल

चलिए देखते हैं कि आसान ग्राफ़ के लिए, C++ ग्राफ़ बिल्डर का इस्तेमाल कैसे किया जा सकता है:

# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"

# Graph outputs.
output_stream: "output_tensors"

node {
  calculator: "InferenceCalculator"
  input_stream: "TENSORS:input_tensors"
  input_side_packet: "MODEL:model"
  output_stream: "TENSORS:output_tensors"
  options: {
    [drishti.InferenceCalculatorOptions.ext] {
      # Requesting GPU delegate.
      delegate { gpu {} }
    }
  }
}

ऊपर दिए गए CalculatorGraphConfig को बनाने के लिए फ़ंक्शन ऐसा दिख सकता है:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  // Requesting GPU delegate.
  inference_opts.mutable_delegate()->mutable_gpu();
  input_tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  Stream<std::vector<Tensor>> output_tensors =
      inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

कम शब्दों में जवाब:

  • Stream/SidePacket के तौर पर ग्राफ़ इनपुट पाने के लिए, Graph::In/SideIn का इस्तेमाल करें
  • नोड के आउटपुट को Stream/SidePacket के तौर पर पाने के लिए, Node::Out/SideOut का इस्तेमाल करें
  • स्ट्रीम और साइड पैकेट को कनेक्ट करने के लिए, Stream/SidePacket::ConnectTo का इस्तेमाल करें नोड इनपुट (Node::In/SideIn) और ग्राफ़ आउटपुट (Graph::Out/SideOut)
    • "शॉर्टकट" है इसके बजाय, ऑपरेटर >> का इस्तेमाल करें ConnectTo फ़ंक्शन (उदाहरण के लिए, x >> node.In("IN")).
  • Stream/SidePacket::Cast का इस्तेमाल, AnyType की स्ट्रीम या साइड पैकेट को कास्ट करने के लिए किया जाता है (उदाहरण के लिए Stream<AnyType> in = graph.In(0);) किसी खास तरह के लिए
    • AnyType के बजाय वास्तविक प्रकारों का उपयोग करने से आप इसकी मदद से, ग्राफ़ बिल्डर की सुविधाएं इस्तेमाल की जा सकती हैं और ग्राफ़ को बेहतर बनाया जा सकता है रीडबिलिटी.

बेहतर इस्तेमाल के लिए

यूटिलिटी फ़ंक्शन

चलिए, अनुमान बनाने वाले कोड को एक खास यूटिलिटी फ़ंक्शन में एक्सट्रैक्ट करते हैं, जिसकी मदद से ये काम किए जा सकते हैं रीडबिलिटी और कोड को दोबारा इस्तेमाल करने के लिए:

// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
    Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
    const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  *inference_opts.mutable_delegate() = delegate;
  tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  InferenceCalculatorOptions::Delegate delegate;
  delegate.mutable_gpu();
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

इस वजह से, RunInference साफ़ इंटरफ़ेस में बताता है कि इनपुट/आउटपुट और उनके टाइप.

इसे आसानी से फिर से इस्तेमाल किया जा सकता है, उदाहरण के लिए लेकिन अगर आपको एक से ज़्यादा पैरामीटर को चलाना है, मॉडल का अनुमान:

  // Run first inference.
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);
  // Run second inference on the output of the first one.
  Stream<std::vector<Tensor>> extra_output_tensors =
      RunInference(output_tensors, extra_model, delegate, graph);

आपको नामों और टैग को डुप्लीकेट करने की ज़रूरत नहीं है (InferenceCalculator, TENSORS, MODEL) या अलग-अलग कॉन्सटेंट के बारे में बताएं - वे जानकारी RunInference फ़ंक्शन में स्थानीय भाषा में है.

यूटिलिटी क्लासेस

यह बात सिर्फ़ फ़ंक्शन की नहीं है, बल्कि कुछ मामलों में यूटिलिटी क्लास की शुरुआत करें, जो आपके ग्राफ़ बनाने वाले कोड को बनाने में मदद कर सकती है और पढ़ने में कम समय लगता है. साथ ही, गड़बड़ी की आशंका भी कम होती है.

MediaPipe, PassThroughCalculator कैलकुलेटर ऑफ़र करता है, जो सिर्फ़ पास हो रहा है मदद मिल सकती है:

input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"

output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"

node {
  calculator: "PassThroughCalculator"
  input_stream: "float_value"
  input_stream: "int_value"
  input_stream: "bool_value"
  # The order must be the same as for inputs (or you can use explicit indexes)
  output_stream: "passed_float_value"
  output_stream: "passed_int_value"
  output_stream: "passed_bool_value"
}

चलिए, ऊपर दिया गया ग्राफ़ बनाने के लिए, आसान C++ कंस्ट्रक्शन कोड देखते हैं:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  auto& pass_node = graph.AddNode("PassThroughCalculator");
  float_value.ConnectTo(pass_node.In("")[0]);
  int_value.ConnectTo(pass_node.In("")[1]);
  bool_value.ConnectTo(pass_node.In("")[2]);
  Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
  Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
  Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

हालांकि, pbtxt फ़ंक्शन में गड़बड़ी होने की संभावना होती है (जब हमारे पास पास करने के लिए कई इनपुट हों इनके ज़रिए, C++ कोड और भी खराब दिखता है: बार-बार खाली टैग होना और Cast कॉल करना. आइए पेश है PassThroughNodeBuilder:

class PassThroughNodeBuilder {
 public:
  explicit PassThroughNodeBuilder(Graph& graph)
      : node_(graph.AddNode("PassThroughCalculator")) {}

  template <typename T>
  Stream<T> PassThrough(Stream<T> stream) {
    stream.ConnectTo(node_.In(index_));
    return node_.Out(index_++).Cast<T>();
  }

 private:
  int index_ = 0;
  GenericNode& node_;
};

और अब ग्राफ़ बनाने वाला कोड ऐसा दिख सकता है:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  PassThroughNodeBuilder pass_node_builder(graph);
  Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
  Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
  Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

अब आप अपने पास थ्रू निर्माण में गलत ऑर्डर या इंडेक्स नहीं डाल सकते कोड बनाएं और PassThrough से Cast के लिए टाइप का अनुमान लगाकर कुछ टाइपिंग सेव करें इनपुट.

क्या करें और क्या न करें

अगर हो सके, तो ग्राफ़ के इनपुट को शुरुआत में ही परिभाषित करें

नीचे दिए गए कोड में:

  • इस बात का अनुमान लगाना मुश्किल हो सकता है कि ग्राफ़ में आपके पास कितने इनपुट हैं.
  • कभी-कभी गड़बड़ी हो सकती है. साथ ही, आने वाले समय में इसका रखरखाव करना मुश्किल हो सकता है (उदाहरण के लिए, क्या यह सही इंडेक्स है? नाम? क्या होगा अगर कुछ इनपुट हटा दिए जाएं या वैकल्पिक कर दिए जाएं? वगैरह).
  • RunSomething का इस्तेमाल सीमित है, क्योंकि दूसरे ग्राफ़ में अंतर हो सकता है इनपुट

नहीं करें — खराब कोड का उदाहरण.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();  // Bad.
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  // 10/100/N lines of code.
  Stream<B> b = graph.In(1).SetName("b").Cast<B>()  // Bad.
  Stream<D> d = RunSomething(a, b, graph);
  // ...

  return graph.GetConfig();
}

इसके बजाय, अपने ग्राफ़ बिल्डर की शुरुआत में अपने ग्राफ़ इनपुट को परिभाषित करें:

DO — अच्छे कोड का उदाहरण.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  Stream<B> b = graph.In(1).SetName("b").Cast<B>();
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();

  // 10/100/N lines of code.
  Stream<D> d = RunSomething(a, b, c, graph);
  // ...

  return graph.GetConfig();
}

std::optional का इस्तेमाल तब करें, जब आपके पास कोई इनपुट स्ट्रीम या साइड पैकेट है जो हमेशा परिभाषित करें और इसे शुरुआत में रखें:

DO — अच्छे कोड का उदाहरण.

std::optional<Stream<A>> a;
if (needs_a) {
  a = graph.In(0).SetName(a).Cast<A>();
}

ग्राफ़ के आउटपुट को सबसे आखिर में तय करना

नीचे दिए गए कोड में:

  • इस बात का अनुमान लगाना मुश्किल हो सकता है कि ग्राफ़ में आपके पास कितने आउटपुट हैं.
  • कभी-कभी गड़बड़ी हो सकती है. साथ ही, आने वाले समय में इसका रखरखाव करना मुश्किल हो सकता है (उदाहरण के लिए, क्या यह सही इंडेक्स है? नाम? क्या होगा अगर कुछ आउटपस हटा दिए जाएं या वैकल्पिक बनाए जाएं? वगैरह).
  • RunSomething का इस्तेमाल सीमित है, क्योंकि दूसरे ग्राफ़ में अलग-अलग आउटपुट हो सकते हैं

नहीं करें — खराब कोड का उदाहरण.

void RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  node.Out("OUTPUT_F")
      .SetName("output_f").ConnectTo(graph.Out(2));  // Bad.
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  node.Out("OUTPUT_D")
      .SetName("output_d").ConnectTo(graph.Out(0));  // Bad.
  // 10/100/N lines of code.
  node.Out("OUTPUT_E")
      .SetName("output_e").ConnectTo(graph.Out(1));  // Bad.
  // 10/100/N lines of code.
  RunSomething(input, graph);
  // ...

  return graph.GetConfig();
}

इसके बजाय, अपने ग्राफ़ बिल्डर के आखिर में अपने ग्राफ़ आउटपुट तय करें:

DO — अच्छे कोड का उदाहरण.

Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  return node.Out("OUTPUT_F").Cast<F>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
  // 10/100/N lines of code.
  Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
  // 10/100/N lines of code.
  Stream<F> f = RunSomething(input, graph);
  // ...

  // Outputs.
  d.SetName("output_d").ConnectTo(graph.Out(0));
  e.SetName("output_e").ConnectTo(graph.Out(1));
  f.SetName("output_f").ConnectTo(graph.Out(2));

  return graph.GetConfig();
}

नोड को एक-दूसरे से अलग करके रखें

MediaPipe में, पैकेट स्ट्रीम और साइड पैकेट, प्रोसेसिंग की तरह ही काम के हैं नोड. साथ ही, किसी भी नोड इनपुट की शर्त और आउटपुट प्रॉडक्ट के बारे में साफ़ तौर पर बताया गया हो स्ट्रीम और साइड पैकेट के हिसाब से देखें, जो पैदा करता है.

नहीं करें — खराब कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));

  auto& node2 = graph.AddNode("Calculator2");
  node1.Out("OUTPUT").ConnectTo(node2.In("INPUT"));  // Bad.

  auto& node3 = graph.AddNode("Calculator3");
  node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C"));  // Bad.

  auto& node4 = graph.AddNode("Calculator4");
  node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C"));  // Bad.
  node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D"));  // Bad.

  // Outputs.
  node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0));  // Bad.
  node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1));  // Bad.
  node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2));  // Bad.
  node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3));  // Bad.

  return graph.GetConfig();
}

ऊपर दिए गए कोड में:

  • नोड एक-दूसरे से जुड़े होते हैं, जैसे कि node4 को पता है कि इसके इनपुट कहां हैं (node1, node2, node3) से मिल रही है और यह रीफ़ैक्टरिंग को मुश्किल बनाती है, रखरखाव और कोड का दोबारा इस्तेमाल करना
    • इस्तेमाल का ऐसा पैटर्न, प्रोटो रिप्रज़ेंटेशन से डाउनग्रेड होता है, जहां नोड डिफ़ॉल्ट रूप से अलग किए जाते हैं.
  • node#.Out("OUTPUT") कॉल डुप्लीकेट हैं. इसलिए, आपको समझने में परेशानी हो रही है के बजाय क्लीनर नाम का इस्तेमाल कर सकते हैं और एक वास्तविक प्रकार भी दे सकते हैं.

इसलिए, ऊपर बताई गई समस्याओं को ठीक करने के लिए, यह ग्राफ़ कंस्ट्रक्शन कोड लिखा जा सकता है:

DO — अच्छे कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  // `node1` usage is limited to 3 lines below.
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  // `node2` usage is limited to 3 lines below.
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  // `node3` usage is limited to 4 lines below.
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  // `node4` usage is limited to 5 lines below.
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

अब, अगर ज़रूरी हो, तो आसानी से node1 को हटाया जा सकता है. साथ ही, b को एक ग्राफ़ इनपुट बनाया जा सकता है, न कि node2, node3, और node4 में अपडेट ज़रूरी हैं (प्रोटो रिप्रज़ेंटेशन की तरह ही वैसे), क्योंकि वे एक-दूसरे से अलग की हुई होती हैं.

कुल मिलाकर, ऊपर दिया गया कोड, प्रोटो ग्राफ़ को ज़्यादा बारीकी से कॉपी करता है:

input_stream: "a"

node {
  calculator: "Calculator1"
  input_stream: "INPUT:a"
  output_stream: "OUTPUT:b"
}

node {
  calculator: "Calculator2"
  input_stream: "INPUT:b"
  output_stream: "OUTPUT:C"
}

node {
  calculator: "Calculator3"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  output_stream: "OUTPUT:d"
}

node {
  calculator: "Calculator4"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  input_stream: "INPUT_D:d"
  output_stream: "OUTPUT:e"
}

output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"

इसके अलावा, अब दूसरे ग्राफ़ में फिर से इस्तेमाल करने के लिए यूटिलिटी फ़ंक्शन एक्सट्रैक्ट किए जा सकते हैं:

DO — अच्छे कोड का उदाहरण.

Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
  auto& node = graph.AddNode("Calculator1");
  a.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<B>();
}

Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
  auto& node = graph.AddNode("Calculator2");
  b.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<C>();
}

Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
  auto& node = graph.AddNode("Calculator3");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  return node.Out("OUTPUT").Cast<D>();
}

Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
  auto& node = graph.AddNode("Calculator4");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  d.ConnectTo(node.In("INPUT_D"));
  return node.Out("OUTPUT").Cast<E>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

आसानी से पढ़ने के लिए अलग-अलग नोड

नहीं करें — खराब कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();
  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

ऊपर दिए गए कोड में, इस आइडिया को समझना मुश्किल हो सकता है कि हर नोड कहां से शुरू होता है और खत्म होता है. इसे बेहतर बनाने और अपने कोड रीडर की मदद करने के लिए, हर नोड के पहले और बाद की लाइनें:

DO — अच्छे कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

साथ ही, ऊपर दिया गया प्रतिनिधि CalculatorGraphConfig प्रोटोकॉल से मेल खाता है और बेहतर तरीके से दिखाना है.

अगर नोड को यूटिलिटी फ़ंक्शन में एक्सट्रैक्ट किया जाता है, तो वे फ़ंक्शन के दायरे में आते हैं और यह जानते हैं कि वे कहां से शुरू और कहां खत्म होते हैं, इसलिए जिनमें ये शामिल हों:

DO — अच्छे कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}