用纯功能JavaScript代码处理肮脏的副作用

如果您尝试进行函数式编程,则意味着您很快就会遇到纯函数的概念。 继续进行下去,您会发现喜欢功能样式的程序员似乎对这些功能很着迷。 他们说纯函数使您可以谈论代码。 他们说,纯功能是不太可能发生如此不可预测的作用的实体,以至于它们会引发热核战争。 您还可以从此类程序员那里了解到纯函数提供了引用透明性。 等等-到无穷大。

顺便说一句,函数式程序员是正确的。 纯函数是好的。 但是有一个问题...


该材料的作者(我们将为您提供翻译的内容)想谈谈如何处理纯函数中的副作用。

纯函数问题


纯函数是没有副作用的函数(实际上,这不是纯函数的完整定义,但是我们将返回此定义)。 但是,如果您至少了解编程方面的知识,那么您知道这里最重要的就是副作用。 如果没人能读到这个数字,为​​什么要计算数字Pi到小数点后百位呢? 为了在屏幕上显示某些内容或在打印机上进行打印,或以其他可感知的形式显示它,我们需要从程序中调用适当的命令。 如果什么都无法写入数据库,数据库的用途是什么? 为了确保应用程序的运行,您需要从输入设备读取数据并从网络资源请求信息。 没有副作用,这是不可能完成的。 但是尽管处于这种状态,函数式编程还是围绕纯函数构建的。 那么以功能风格编写程序的程序员如何解决这个矛盾呢?

如果您简而言之回答这个问题,那么函数式程序员就和数学家一样:他们作弊。 尽管尽管有这种指责,但必须说,从技术角度来看,他们只是遵循某些规则。 但是他们在这些规则中发现了漏洞,并将它们扩展到了令人难以置信的大小。 他们通过两种主要方式执行此操作:

  1. 他们利用了依赖注入。 我称它在围栏上引发了问题。
  2. 他们使用函子,在我看来,这是一种极端的拖延形式。 在这里应该注意,在Haskell中,它被称为“ IO函数”或“ IO monad”,在PureScript中,使用了“效果”一词, 我认为 ,这对于描述函子的本质更好一些。

依赖注入


依赖注入是处理副作用的第一种方法。 使用这种方法,我们可以处理所有会污染代码的内容,并将其放入函数的参数中。 然后,我们可以将所有这些视为其他功能的一部分。 我将通过以下示例对此进行解释:

// logSomething :: String -> String function logSomething(something) {    const dt = (new Date())toISOString();    console.log(`${dt}: ${something}`);    return something; } 

在这里,我想为那些熟悉类型签名的人做笔记。 如果我们严格遵守规则,那么我们将不得不考虑副作用。 但是我们稍后会处理。

logSomething()函数有两个问题使其无法声明为“干净”状态:它创建一个Date对象并将某些内容输出到控制台。 也就是说,我们的函数不仅执行输入输出操作,而且在不同时间调用它时,还会产生不同的结果。

如何使此功能清洁? 使用依赖注入技术,我们可以处理污染函数的所有内容并将其设为函数参数。 结果,我们的函数将接受三个参数,而不是接受一个参数:

 // logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) {   const dt = d.toIsoString();   return cnsl.log(`${dt}: ${something}`); } 

现在,为了调用该函数,我们需要将所有污染它的东西都转移到它之前:

 const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something); //  "Curiouser and curiouser!" 

在这里,您可能会认为所有这些都是胡说八道,我们仅将问题上移了一层,而这并没有增加代码的纯度。 而且您知道,这些是正确的想法。 这是最纯粹形式的漏洞。

这就像是假装的无知:“我不知道调用cnsl对象的log方法cnsl导致执行I / O语句。 有人刚把它交给我,但我不知道这一切都来自哪里。” 这种态度是错误的。

而且,实际上,发生的事情并不像乍看上去那样愚蠢。 看一下logSomething()函数的功能。 如果您想做一些不清洁的事情,那么您必须自己做。 假设您可以将各种参数传递给此函数:

 const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = {   log: () => {       //      }, }; logSomething(d, cnsl, "Off with their heads!"); //   "Off with their heads!" 

现在我们的函数什么都不做(它只返回something参数)。 但是她是完全纯洁的。 如果多次使用相同的参数调用它,则每次都会返回相同的内容。 这就是重点。 为了使此功能不干净,我们需要有意识地执行某些操作。 或者,换句话说,函数依赖的所有内容都在其签名中。 它不访问任何全局对象,例如consoleDate 。 一切都正式化了。

此外,需要注意的是,我们可以将其他函数转移到以前不干净的函数中,这一点很重要。 再看一个例子。 想象一下,以某种形式存在一个用户名,我们需要获取该形式的相应字段的值:

 // getUserNameFromDOM :: () -> String function getUserNameFromDOM() {   return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; //   "mhatter" 

在这种情况下,我们试图从DOM加载一些信息。 纯函数不会这样做,因为document是可以随时更改的全局对象。 使此类函数整洁的一种方法是将全局document对象作为参数传递给它。 但是,您仍然可以将querySelector()函数传递给querySelector() 。 看起来像这样:

 // getUserNameFromDOM :: (String -> Element) -> String function getUserNameFromDOM($) {   return $('#username').value; } // qs :: String -> Element const qs = document.querySelector.bind(document); const username = getUserNameFromDOM(qs); username; //   "mhatter" 

再一次,您可能会想到这是愚蠢的。 毕竟,这里我们只是简单地从getUsernameFromDOM()函数中删除了不允许我们将其清洁的内容。 但是,我们并没有摆脱它,只是将对DOM的调用转移到另一个函数qs() 。 看来,此步骤的唯一值得注意的结果是新代码比旧代码长。 现在,我们有两个函数,而不是一个不干净的函数,其中一个仍然不干净。

等一下 想象一下,我们需要为getUserNameFromDOM()函数编写一个测试。 现在,比较此功能的两个选项,考虑使用哪个更容易使用? 为了使该函数的脏版本完全起作用,我们需要一个全局文档对象。 此外,该文档应具有带有username标识符的元素。 如果需要在浏览器外部测试类似功能,则需要使用JSDOM之类的工具或没有用户界面的浏览器。 请注意,所有这些仅用于测试长度为几行的小功能。 为了测试此功能的第二个原始版本,只需执行以下操作:

 const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`); 

当然,这并不意味着要测试这些功能,就不需要在真实的浏览器中执行集成测试(或至少使用JSDOM之类的工具)。 但是此示例展示了一个非常重要的事情,那就是现在getUserNameFromDOM()函数getUserNameFromDOM()变得完全可以预测。 如果将qsStub()传递给它,它将始终返回mhatter 。 我们已经将“不可预测性”移至小函数qs()

如有必要,我们可以将不可预测的机制带入与主要功能更远的层次。 因此,相对而言,我们可以将它们移到代码的“边界区域”。 这将导致我们有一个薄薄的不干净代码外壳,包围着经过良好测试和可预测的内核。 随着程序员创建的项目规模的增长,代码的可预测性变得非常有价值。

the依赖项注入机制的缺点


使用依赖注入,您可以编写一个大型而复杂的应用程序。 我知道这一点,因为我自己编写了这样一个应用程序 。 通过这种方法,简化了测试,并且功能依赖性变得清晰可见。 但是依赖注入并非没有缺陷。 主要的一点是,当使用它时,可以获得很长的功能签名:

 function app(doc, con, ftch, store, config, ga, d, random) {   //     } app(document, console, fetch, store, config, ga, (new Date()), Math.random); 

实际上,这还不错。 如果某些参数需要传递给某些功能,而这些功能却深深地嵌入到其他功能中,则将体现出这种构造的缺点。 看起来需要通过许多级别的函数调用传递参数。 当此类级别的数量增加时,它就会变得烦人。 例如,可能有必要通过5个中间函数传送表示日期的对象,而没有一个中间函数使用此对象。 当然,尽管不能说这种情况就像一场普遍的灾难。 另外,这使得可以清楚地看到功能的依赖性。 但是,尽管如此,这仍然不是那么令人愉快。 因此,我们考虑以下机制。

▍懒函数


让我们看一下函数式编程的拥护者所使用的第二个漏洞。 它包含以下思想:副作用在实际发生之前不是副作用。 我知道这听起来很神秘。 为了弄清楚这一点,请考虑以下示例:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

我知道一个例子可能是一个愚蠢的例子。 如果我们需要数字0,则为了使其出现,只需在代码中的正确位置输入它即可。 我也知道您不会编写用于控制核武器的JavaScript代码。 但是我们需要此代码来说明所涉及的技术。

因此,这是一个不纯函数的示例。 它将数据输出到控制台,这也是核战争的原因。 但是,假设我们需要此函数返回的零。 想象一个场景,我们需要在发射火箭之后计算一些东西。 假设我们可能需要启动倒数计时器或类似的计时器。 在这种情况下,事先考虑进行计算将是完全自然的。 而且我们必须确保火箭准确地在需要时发射。 我们不需要以可能导致意外发射火箭的方式进行计算。 因此,让我们考虑一下如果将fZero()函数包装在另一个仅返回它的函数中会发生什么。 假设这将是一个安全包装器:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   return fZero; } 

您可以根据需要returnZeroFunc()调用returnZeroFunc()函数。 在这种情况下,在执行返回值之前,我们(理论上)是安全的。 在我们的情况下,这意味着执行以下代码不会导致核战争:

 const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc(); //     . 

现在比以前更严格一些,让我们研究“纯函数”一词的定义。 这将使我们能够更详细地检查returnZeroFunc()函数。 因此,该函数在以下情况下是干净的:

  • 没有观察到副作用。
  • 链接透明度。 也就是说,使用相同的输入值调用此类函数始终会导致相同的结果。

returnZeroFunc()分析returnZeroFunc()函数。

她有副作用吗? 我们刚刚发现,调用returnZeroFunc()不会发射导弹。 如果不调用此函数返回的内容,则不会发生任何事情。 因此,我们可以得出结论,此功能没有副作用。

此功能参照透明吗? 也就是说,将相同的输入数据传递给它时,它是否总是返回相同的值? 我们将利用以下代码段中多次调用此函数的事实进行验证:

 zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true 

一切看起来不错,但是returnZeroFunc()函数尚未完全清除。 她指的是超出自己范围的变量。 为了解决这个问题,我们重写函数:

 // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   function fZero() {       console.log('Launching nuclear missiles');       //              return 0;   }   return fZero; } 

现在,该功能可以认为是干净的。 但是,在这种情况下,JavaScript规则不利于我们。 即,我们不再可以使用===运算符检查函数的引用透明性。 这是由于returnZeroFunc()将始终返回对该函数的新引用这一事实。 的确,可以通过亲自检查代码来验证链接的透明度。 这样的分析将表明,每次调用函数都会返回到同一函数的链接。

摆在我们面前的是一个整洁的小漏洞。 但是可以在实际项目中使用吗? 这个问题的答案是肯定的。 但是,在讨论如何在实践中使用此功能之前,我们将稍微提出一些想法。 即, fZero()回到危险函数fZero()

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

我们将尝试使用此函数返回的零,但我们将这样做以确保(到目前为止)核战争不会开始。 为此,创建一个函数,该函数采用fZero()函数返回的零并将其添加一个:

 // fIncrement :: (() -> Number) -> Number function fIncrement(f) {   return f() + 1; } fIncrement(fZero); //      //   1 

真倒霉。我们不小心发动了核战争。 让我们再试一次,但是这次我们不会返回数字。 相反,我们返回某个函数,该函数有朝一日返回一个数字:

 // fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) {   return () => f() + 1; } fIncrement(zero); //   [Function] 

现在您可以放心了。 灾难避免了。 我们继续研究。 由于这两个功能,我们可以创建一大堆“可能的数字”:

 const fOne   = fIncrement(zero); const fTwo   = fIncrement(one); const fThree = fIncrement(two); //   … 

此外,我们可以创建许多名称以f开头的f (我们称它们为f*()函数),旨在与“可能的数字”一起使用:

 // fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) {   return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) {   return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) {   return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); //    ,   . ! 

看看我们在这里做了什么? 使用“可能的数字”,您可以执行与使用普通数字相同的操作。 数学家称之为同构 。 通过将普通数字置于函数中,可以始终将其转换为“可能的数字”。 您可以通过调用该函数来获得“可能的号码”。 换句话说,我们在常规数和“可能数”之间建立了映射。 实际上,这比看起来有趣得多。 很快我们将回到这个想法。

上面使用包装函数的技术是有效的策略。 我们可以根据需要尽可能多地隐藏函数。 而且,由于我们尚未调用任何这些函数,因此从理论上讲,所有这些函数都是纯函数。 没有人发动战争。 在常规代码中(与火箭无关),最终我们实际上需要副作用。 将我们需要的所有内容包装到函数中,使我们可以精确地控制这些效果。 我们选择这些效果出现的时间。

应该注意的是,在各处使用带括号的统一结构来声明函数并不是很方便。 并且创建每个功能的新版本也不是一件令人愉快的事情。 JavaScript具有一些很棒的内置函数,例如Math.sqrt() 。 如果有一种方法可以将这些普通功能与我们的“待处理值”一起使用,那就太好了。 实际上,我们现在将讨论这个。

函子效应


在这里,我们将讨论由包含“延迟函数”的对象表示的函子。 为了表示函子,我们将使用Effect对象。 我们将把函数fZero()放在这样的对象中。 但是,在执行此操作之前,我们将使此功能更加安全:

 // zero :: () -> Number function fZero() {   console.log('Starting with nothing');   //  , ,     .   //       .   return 0; } 

现在,我们描述用于创建类型为Effect对象的构造函数:

 // Effect :: Function -> Effect function Effect(f) {   return {}; } 

这里没有什么特别有趣的,因此我们将继续使用此功能。 因此,我们想对Effect对象使用通常的fZero()函数。 为了提供这种情况,我们将编写一个接受常规函数的方法,并有朝一日将其应用于“待定值”。 我们将在不调用Effect函数的情况下执行此操作。 我们称这样的函数map() 。 它之所以这样命名,是因为它在常用功能和Effect功能之间建立了映射。 它可能看起来像这样:

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       }   } } 

现在,如果您密切监视正在发生的事情,则可能对map()函数有疑问。 它看起来与这首歌可疑相似。 我们稍后会再讨论此问题,但现在我们将测试目前的操作:

 const zero = Effect(fZero); const increment = x => x + 1; //   . const one = zero.map(increment); 

所以...现在我们没有机会观察这里发生的事情。 因此,让我们修改“ Effect以便可以说有机会“拉动触发器”:

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }   } } const zero = Effect(fZero); const increment = x => x + 1; //  . const one = zero.map(increment); one.runEffects(); //       //   1 

如有必要,我们可以继续调用map()函数:

 const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero)   .map(increment)   .map(double)   .map(cube); eight.runEffects(); //       //   8 

在这里,正在发生的事情已经开始变得越来越有趣。 我们称其为“ functor”。 这一切都意味着Effect对象具有map()函数,并且遵守一些规则 。 但是,这些不是禁止任何内容的规则。 这些规则是关于您可以做什么的。 它们更像是特权。 由于Effect对象是一个仿函数,因此它遵守这些规则。 特别地,这就是所谓的“组成规则”。

看起来像这样:

如果有一个名为eEffect对象以及两个函数fg ,则e.map(g).map(f)等效于e.map(x => f(g(x)))

换句话说,两个连续的map()方法等效于组成两个函数。 这意味着类型为Effect的对象可以执行类似于以下操作(请记住上面的示例之一):

 const incDoubleCube = x => cube(double(increment(x))); //       Ramda  lodash/fp      : // const incDoubleCube = compose(cube, double, increment); const eight = Effect(fZero).map(incDoubleCube); 

当我们执行此处显示的操作时,可以保证得到的结果与使用此代码的版本以及对map()的三次调用所获得的结果相同。 我们可以在重构代码时使用它,并且可以确保代码可以正常工作。 在某些情况下,将一种方法更改为另一种方法甚至可以提高性能。

现在,我建议停止试验数字,并讨论看起来更像实际项目中使用的代码的事物。

(()的方法


Effect对象的构造函数接受一个函数作为参数。 这很方便,因为我们要推迟的大多数副作用都是函数。 例如,这些是Math.random()console.log() 。 但是,有时您需要在不是函数的Effect对象中放置一个值。 , , window . , . , ( -, , , Haskell pure ):

 // of :: a -> Effect a Effect.of = function of(val) {   return Effect(() => val); } 

, , , -. , , . HTML- . , . . 例如:

 window.myAppConf = {   selectors: {       'user-bio':     '.userbio',       'article-list': '#articles',       'user-name':    '.userfullname',   },   templates: {       'greet':  'Pleased to meet you, {name}',       'notify': 'You have {n} alerts',   } }; 

, Effect.of() , Effect :

 const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']); //   Effect('.userbio') 

▍ Effect


. , Effect . , getElementLocator() , Effect , . DOM, document.querySelector() — , . :

 // $ :: String -> Effect DOMElement function $(selector) {   return Effect.of(document.querySelector(s)); } 

, , map() :

 const userBio = userBioLocator.map($); //   Effect(Effect(<div>)) 

, , . div , map() , , . , innerHTML , :

 const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML)); //   Effect(Effect('<h2>User Biography</h2>')) 

, . userBio , . , , , . , , Effect('user-bio') . , , , :

 Effect(() => '.userbio'); 

— . :

 Effect(() => window.myAppConf.selectors['user-bio']); 

, map() , ( ). , , $ , :

 Effect(() => $(window.myAppConf.selectors['user-bio'])); 

, :

 Effect(   () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) ); 

Effect.of , :

 Effect(   () => Effect(       () => document.querySelector(window.myAppConf.selectors['user-bio'])   ) ); 

, , , . Effect .

▍ join()


? , Effect . , , .

Effect .runEffect() . . , - , , , , . , . join() . Effect , runEffect() , . , .

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }   } } 

, :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .map($)   .join()   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

▍ chain()


, .map() , .join() , . , , . , , Effect . , .map() .join() . , , Effect :

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }       chain(g) {           return Effect(f).map(g).join();       }   } } 

chain() - , , Effect ( , ). HTML- :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .chain($)   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

-. . , flatMap . , , — , , join() . Haskell, , bind . , - , , chain , flatMap bind — .

▍ Effect


Effect , . . , DOM, , ? , , , . , . — .

 // tpl :: String -> Object -> String const tpl = curry(function tpl(pattern, data) {   return Object.keys(data).reduce(       (str, key) => str.replace(new RegExp(`{${key}}`, data[key]),       pattern   ); }); 

. :

 const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str}); //   Effect({name: 'Mr. Hatter'}); const pattern = win.map(w => w.myAppConfig.templates('greeting')); //   Effect('Pleased to meet you, {name}'); 

, . . ( name pattern ) Effect . tpl() , , Effect .
, map() Effect tpl() :

 pattern.map(tpl); //   Effect([Function]) 

, . map() :

 map :: Effect a ~> (a -> b) -> Effect b 

:

 tpl :: String -> Object -> String 

, map() pattern , ( , tpl() ) Effect .

 Effect (Object -> String) 

pattern Effect . . Effect , . ap() :

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }       chain(g) {           return Effect(f).map(g).join();       }       ap(eff) {            //  -  ap,    ,   eff   (  ).           //    map  ,    eff       (  'g')           //   g,     f()           return eff.map(g => g(f()));       }   } } 

.ap() :

 const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str})); const pattern = win.map(w => w.myAppConfig.templates('greeting')); const greeting = name.ap(pattern.map(tpl)); //   Effect('Pleased to meet you, Mr Hatter') 

, … , , .ap() . , , map() , ap() . , , .

. , . , , , Effect , ap() . , :

 // liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) {   return y.ap(x.map(f));   //      :   // return x.map(f).chain(g => y.map(g)); }); 

liftA2() , , . liftA3() :

 // liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d) const liftA3 = curry(function liftA3(f, a, b, c) {   return c.ap(b.ap(a.map(f))); }); 

, liftA2() liftA3() Effect . , , ap() .

liftA2() :

 const win = Effect.of(window); const user = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str}); const pattern = win.map(w => w.myAppConfig.templates['greeting']); const greeting = liftA2(tpl)(pattern, user); //   Effect('Pleased to meet you, Mr Hatter') 

?


, , , . ? , Effect ap() . , ? ?

: « , , ».

:

  • — ?
  • , Effect , ?


— . , , , . const pattern = window.myAppConfig.templates['greeting']; , , , :

 const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting')); 

— , , , , . . — , , . , , , , , , . , . — . , , , . , .

. .

▍ Effect


, , . - Facebook Gmail . ? .

, . . CSV- . . , , , . , . , . , , , .

, . , map() reduce() , . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .

, , . , . 看起来不一样吗? , , , . . , .

TensorFlow , .

TensorFlow, , . «». , , :

 node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2) 

Python, JavaScript. , Effect , add() , ( sess.run() ).

 print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) #  node3:  Tensor("Add_2:0", shape=(), dtype=float32) #  sess.run(node3):  7.0 

, (7.0) , sess.run() . , . , , , .

总结


, . , . Effect .
, , , , , . , , . Effect , , , . , .

— . , . , , . . . , .



亲爱的读者们! ?

Source: https://habr.com/ru/post/zh-CN422589/


All Articles